Shared webview-side pieces for the Photoshop UXP panels uxp_psdrename, uxp_psdtext and uxp_psdexport.
All three put their UI in a webview and keep the UXP panel script as a thin
bridge to Photoshop, so they kept growing the same helpers three times over —
and a fix in one silently left the others broken. This repository is where
those helpers live now. It is used as a submodule at
plugin/webview/common/, inside the webview root because a webview may not
be able to reach above it.
| Module | What it does |
|---|---|
bridge.js |
Talking to the UXP panel over postMessage: requests as promises, notifications to handlers, and the retries the real thing turned out to need |
modal.js |
Opening and closing modal dialogs: backdrop clicks, Escape, and a guard for unsaved edits |
paste.js |
Pasting through the panel, because keystrokes do not reach a freshly opened panel on Windows |
wake.js |
The one-time hint telling the user to click Photoshop once, for the same reason |
i18n.js |
A tiny i18n engine; each panel supplies its own dictionary |
diag.js |
The one-line diagnostics readout, written without touching the bridge so it still works when the bridge does not |
Plain ES modules with no build step and no dependencies.
import { createBridge } from './common/bridge.js';
const bridge = createBridge({
iid: IID,
timeoutMessage: (type) => tr('app.timeout', type),
handlers: {
tree: (msg) => applyTree(msg),
log: (msg) => dlog('panel', msg.msg),
showHelp: () => openHelp(),
},
isConnected: () => state.connected,
onSendError: () => { if (!state.connected) renderAll(); },
onGiveUp: () => { bridgeFailed = true; renderAll(); },
});
const { post, request } = bridge;
bridge.connect();request(type, payload) resolves with the reply that carries the same
reqId, and rejects if nothing comes back — otherwise a silent panel leaves
the promise pending and a button disabled for good. Messages without a
reqId go to handlers, keyed by type; a message that is both a reply and
a notification (a layer tree, say) does both.
Three things here come from what the panel actually does rather than what the
documentation says. uxpHost.postMessage accepts a different shape depending
on the environment, so sends are tried three ways. Replies arrive as a
message event on window, not on the <webview> element, where data
comes through undefined. And the first ready can be sent before the bridge
is up, so connect() keeps resending until the panel answers, then gives up
and says so rather than hanging silently.
bridge.stats carries sendTries and lastSendError for a diagnostics line.
click fires on the nearest common ancestor of mousedown and mouseup, so
a selection drag that starts inside a dialog and ends on the backdrop reads as
a backdrop click. Closing on that alone throws away whatever was being edited.
wireModalClose closes only when the press and the release are both on the
backdrop, and routes Escape through the same door.
import { wireModalClose, escapeModal } from './common/modal.js';
wireModalClose('#editDialog', closeEditDialog, isDirty,
() => setStatus('#editStatus', tr('modal.dirty'), 'error'));
// Escape: pass the dialogs front-most first
escapeModal(['#helpDialog', '#sheetDialog', '#editDialog']);Pass isDirty only for dialogs that lose work when they close. The ×
button always closes, since pressing it is deliberate.
On Windows a freshly opened panel receives no keystrokes at all, so Ctrl+V
never arrives — a known Adobe issue with no workaround. The panel side reads
the clipboard instead and hands the text over; a button click is handled by
UXP and needs no keyboard delivery. Clicking a field still moves DOM focus
even when the host window is not active, so "the field you clicked last" is
enough to know where the text should land.
import { attachPaste } from './common/paste.js';
attachPaste('#renameDialog', {
button: '#rnPaste', fallback: '#rnFind',
request, tr, setStatus: setRenameStatus,
});The panel must hold the clipboard permission and answer a readClipboard
message with { text }.
The same Windows problem seen from the other side. Until the host window is
activated, a panel opened moments ago gets no keystrokes — not Ctrl+V, not
ordinary typing. Clicks arrive and DOM focus lands on the field, so nothing
looks wrong; the keys simply go nowhere. One click on Photoshop itself wakes
it, and everything behaves normally from then on. It is not specific to the
webview: a native field on the panel does the same. There is no API to fix
it, so the panel says so once.
import { createWakeHint } from './common/wake.js';
createWakeHint();The panel supplies the element (#wakeHint by default) and its wording, the
way diag.js takes #diagLine. The hint clears itself the moment a key
arrives, which makes its presence the signal: still showing means still
asleep. Clicking it dismisses it too. Nothing is shown on macOS, where the
problem does not exist.
import { createI18n } from './common/i18n.js';
const DICT = { ja: { 'app.lang': 'EN' }, en: { 'app.lang': 'JA' } };
export const { tr, applyI18n, currentLang, toggleLang, setLang } =
createI18n(DICT, 'psdrename.lang');applyI18n() fills every data-i18n, data-i18n-title and data-i18n-ph
element. tr('rn.count', 10, 3) substitutes {0} and {1}. English is the
default, and an unknown key falls back to English and then to the key itself.
localStorage is unavailable inside a UXP webview, so the language lives in
memory; setLang() exists for panels that keep it in their own preferences.
A line pinned to the bottom of the panel showing what the visible context actually believes: instance id, a ticking clock, connection state, send counters, and the last error. It deliberately avoids the bridge and the network, so it keeps working when those are what is broken. A stopped clock means compositing itself has frozen.
import { newIid, createDiag } from './common/diag.js';
const IID = newIid();
const diag = createDiag({
iid: IID,
stats: bridge.stats,
fields: () => ' conn:' + (state.connected ? 'Y' : 'n') +
' rows:' + state.rows.length,
});
diag.toggle(); // from Ctrl+D, and from the panel flyout menuReach it from the flyout as well as the keyboard: on Windows a webview does not reliably get keyboard focus, so a keyboard-only toggle is unavailable exactly when the line is wanted.