In this episode weāll learn how to build Modal Dialogs for UXP plugins. Feel free to watch the video or read the article, they cover the same ground.
ExtendScript history
ExtendScript was the language that weāve been using to script Adobe applications literally forever. It was based on the ECMAScript spec version 3 (a.k.a. ES3), that was cutting edge in the decade 1999-2009. Trivia moment: ECMA stands for European Computer Manufacturers Association, and itās the organization that standardized the JavaScript language under the name ECMAScript. Thereās a terrific interview with Brendan Eich (who famously created JavaScript in a hurry in just 10 days) in Lex Fridmanās channel, check that out ā trust me, the man is quite something.
So in a way JavaScript and ExtendScript share a ācommon ancestorā, ES3, but their evolution diverged. Adobe extended (hence the name) the ES3 spec with extra features e.g. the E4X library for XML literals support; while JavaScript went on with ES5 (JSON support, Array methodsā¦), ES6 (new syntaxā¦), etc.
Fast forward to the Adobe MAX 2020 conference and the adoption of a new, unified UI/Scripting engine in UXP. All the ExtendScript extras that we were familiar with are now gone: disappeared, no more. This is the price weāve got to pay to have a modern JavaScript engine ā fair enough. Some fellow developers might not have realized this because there was a lot to sink in, but probably the most conspicuous feature thatās gone missing is the entire ScriptUI class, that has always been the principal tool to build script-driven Windows both in the CEP era and also way before when the Cloud was not around and all we had was a Creative Suite.
Letās be clear about terminology before going any further, so that no matter what your background is weāre all on the same page:
- Dialog: an independent window that pops up and can be:
- Modal: it steals the focus, you canāt interact with any other UI element but the dialog itself.
- Modeless: the dialog stays around and youāre allowed to deal with the rest of the Photoshop interface.
The concept of Modal vs. Modeless UIs is present all over in the Photoshop interface, e.g. Adjustments: on the left a Modal dialog, on the right a Modeless panel.
We used to build modals with ScriptUI Windows of type 'dialog'
, and in theory modeless with ScriptUI Windows of type 'palette'
, but those werenāt really supported in Photoshop ā they acted weird and weāve been officially advised against their use. Before CEP came about, ScriptUI was enough to build rich UIs:
But as soon as Flash first, then CEP panels landed in Photoshop, ScriptUI was relegated to simpler dialogs or for backward compatibility only. You may not know it, but also those super-handy alert()
, prompt()
and confirm()
dialogs were part of ScriptUI.
// Old ExtendScript code
var res = prompt("What's the meaning of life?\n" +
"Feel free to think as long as you need to...", 42);
Theyāre all gone too, with the exception of alert()
thatās been re-implemented in the Photoshop API as showAlert()
.
// Photoshop API
const photoshop = require("photoshop")
await photoshop.app.showAlert("Get ready!\nRight on");
Even though it doesnāt differentiate between title vs. text yet (theyāre both bold, only the title should be) ā a minor cosmetic glitch.
Anyway, whatever code we used to write in the recent past to create dialogs, thatās gone now ā thanks for all the fish. UXP provides alternatives for both modeless windows (as UXP panels, that Iāve extensively covered) and modal dialogs, which Iām going to show you in a moment.
ā ļø Disclaimer
UXP is a technology under very active development, so the code that Iām showing you may be subject to change in the future: this is the premise of all my work here, given nature of the topic ā rest assured that Iāll post updates if/when needed.
That said, there are features that might be already available for you to use, but not really (either partially or fully) documented yet. Incidentally this happens to be the case of some Dialogs, so I was dubious whether to include them here or not. Iāve decided that I will mention them anyway because IMHO itās crucial for you to plan ahead and be informed about the actual realm of possibilities, so to speak. Although I would suggest not to use those features yet, for the risk that the API will change is even greater ā be warned.
As a general rule, you wonāt hear a word from me about anything that belongs to the prerelease, or that works in beta versions only ā besides breaking all sort of NDAs, it would be pointless. Instead, if the feature is implemented in Photoshop-release I might mention/show it, essentially to inform you but deferring for the actual use to some time in the future when the official documentation covers it fully.
Iāve prepared two different UXP plugins to show you a few of the possible ways to use Dialogs ā one using vanilla JS and the other based on React.js. I havenāt covered React here yet, but it felt wrong to leave that out. You can find both of the examples in my GitHub repository.
Vanilla JS UXP dialogs
This vanilla JS example comes directly from the ps-starter
template that is available from the UXP Developer Tool (see here if you still need pointers).
The base structure is very bare, just some Spectrum UXP components:
<sp-heading>Dialog examples</sp-heading>
<sp-divider size="large" style="margin-bottom:10px"></sp-divider>
<sp-body>
Two different ways to create modal dialogs.<br />
Please note that Spectrum Dialogs API is still a work in progress.
</sp-body>
<div class="row" style="margin: 20px; justify-content: space-around;">
<sp-button onclick="openDialog()">dialog</sp-button>
<sp-button onclick="openSpectrumDialog()">sp-dialog</sp-button>
</div>
<!-- Version Info, to be filled by JS -->
<sp-detail size="L" id="info"></sp-detail>
Letās cover what is officially documented first, which is the result of the first ādialogā button. In order to create a modal dialog, you need to have an actual <dialog>
element in the <body>
first:
<!-- Standard Dialog -->
<dialog id="dialog">
<sp-heading size="L" style="margin: 0">šš» Hello there!</sp-heading>
<sp-divider size="large" style="margin:10px 0"></sp-divider>
<sp-body>This is a rich modal dialog where you can use all
the available UI elements in UXP.</sp-body>
<img src="img/UXP.png" width="400">
<div class="row" style="justify-content: flex-end; margin-top:30px">
<sp-button variant="secondary"
style="margin-right:10px"
onclick="document.querySelector('#dialog').close('NOPE')">
Cancel
</sp-button>
<sp-button variant="primary"
onclick="document.querySelector('#dialog').close('OK')">
Ok
</sp-button>
<!-- ESC will return "reasonCanceled" -->
</div>
</dialog>
The <dialog>
element itself is nothing special, Iāve used Spectrum elements that weāve seen in the past episode like <sp-heading>
, <sp-divider>
and <sp-body>
. Please note that, as Iāve also mentioned in the Spectrum episode, a class
of "row"
or "column"
triggers the display
equals to flex
in the <div>
so itās possible to use FlexBox-specific styles such as justify-content
etc.
The two <sp-button>
elements onclick
handlers both use the close()
method of the dialog, that in turn is retrieved with document.querySelector()
passing the id
, which is in fact "dialog"
(I hope this is not confusing).
Please note that even if the markup code is there, the dialog doesnāt pop up by default. In order to open it letās have a look at the openDialog()
function that is triggered by the pluginās ādialogā button.
const openDialog = async () => {
const res = await document.querySelector("#dialog").uxpShowModal({
title: "About",
resize: "both",
size: {
width: 464,
height: 380
}
})
console.log(`The dialog closed with: ${res}`)
}
So, the dialog is retrieved with the same querySelector()
but to open it we run the UXP-specific uxpShowModal()
method. That accepts an options object with a title
property (the dialogās title bar); a resize
prop, that controls whether the dialog is resizeable (either "horizontal"
, "vertical"
, "both"
or "none"
ā might still be a bit buggy, it was in the past); and size
, an object with width
and height
properties (quite self explanatory).
To sum up: you add to the <body>
one or more <dialog>
elements (that you can get by id
) filled with whatever elements you need. You open the modal dialog with uxpShowModal()
passing an options object, and you can close the dialog with the close()
method optionally passing a return value (in the example Iāve got close('NOPE')
and close('OK')
). Also note that opening a dialog is an asynchronous process (hence openDialog
is an async
callback, and it await
s for the uxpShowModal()
) that allows the result res
to be stored/logged afterwards. If the user cancels, a "reasonCanceled"
string is returned.
Spectrum dialog
Warning: this is, as I said earlier, still undocumented so please wait for the official documentation and/or use with caution.
Instead of a <dialog>
element, thereās the possibility to use a <sp-dialog>
that allows some pre-defined styling as slots. You still place it in the <body>
markup:
<sp-dialog id="sp-dialog">
<sp-heading slot="title" style="margin: 0">Use with caution!</sp-heading>
<sp-icon slot="icon" style="color: #ec5b62" size="m" name="ui:AlertSmall"></sp-icon>
<sp-body slot="description">
Spectrum Dialogs are still undocumented, yet available in PS-release.
<br />
Be aware that this possibility exists and keep an eye on the Doc.
</sp-body>
<sp-button-group slot="buttongroup">
<sp-button variant="primary"
onclick="document.querySelector('#sp-dialog').removeAttribute('open')">
Ok
</sp-button>
</sp-button-group>
</sp-dialog>
But the result is strikingly different:
The modal dialog is blocked in the screenās center (you canāt move it) and the panel gets darkened. There are several slots such as "title"
, "icon"
, "description"
and "buttongroup"
. I wonāt go too much into any of those because, as Iāve mentioned, itās better for us to wait for the official documentation. One major difference is that you donāt use uxpShowModal()
anymore:
const openSpectrumDialog = () => {
document.querySelector("#sp-dialog").setAttribute("open");
}
Instead you set the "open"
attribute of the <sp-dialog>
element. Similarly, as you can see in the dialog āOkā buttonās click handler, to close the dialog you just remove "open"
via removeAttribute()
.
Now you know that this is possible and itāll be soon documented ā you can play with it in Photoshop 22.2. Speaking of versions, if youāre wondering how the Photoshop/UXP line fills:
// fills the versions sp-detail
const uxp = require("uxp");
document.getElementById("info").innerHTML = `Photoshop ${uxp.host.version}, ${uxp.versions.uxp}`
React.js UXP dialog
As I said Iāve not introduced React.js in this series yet ā itās planned ā but I want to include an example anyway. This comes straight from the ps-react-starter
template, cleaned of almost everything; I might assume that you know how to orient yourself in a React project, but If you donāt and want to follow/play with it, please download the React example from this repository, then:
yarn install
Do not npm install
because it wonāt deploy all the needed components. Then point the UXP Developer Tool to the manifest.json
that sits in the projectās dist
folder ā the distribution files come pre-compiled. If you want to modify the code, then you need to:
npm run watch
This will watch the src
folder and bundle with Webpack on each save to dist
: if youāre not familiar with React, usually you code separate components that a bundler (such as WebPack) merges alongside with the required dependencies in one or a few JavaScript files in dist
. Additionally, you need to watch the dist
folder in the UXP Developer Tool so that it knows when to reload the plugin.
The whole example is very simplified, itās a dummy Preferences dialog that doesnāt really saves any preference, but is able to hand it to the UXP panel (I will suggest later on ways to improve it).
Letās concentrate on the only two files that matter here, both in the src
folder: /panels/Dialogs.jsx
, the outer container in the UXP plugin panel; and /components/Preferences.jsx
, the content of the dialog that pops up.
The Dialogs.jsx
file is not very fancy but there are two or three things Iād like to point out.
import React from "react";
import ReactDOM from 'react-dom'
import Preferences from "../components/Preferences"
import uxp from "uxp"
const Dialogs = () => {
let preferencesDialog = null;
const openPreferences = async () => {
// Make it better, e.g. loading data from localStorage passing it to the
// dialog, and saving it thereafter
if (!preferencesDialog) {
preferencesDialog = document.createElement("dialog");
ReactDOM.render(<Preferences dialog={preferencesDialog} />, preferencesDialog);
}
document.body.appendChild(preferencesDialog);
let result = await preferencesDialog.uxpShowModal({
title: "Preferences",
resize: "both",
size: {
width: 300,
height: 250
}
});
// Do whatever you need with the {smartObject, vectorContent} result
console.log(result)
preferencesDialog.remove();
}
return (
<div>
<sp-heading>Dialog example in React</sp-heading>
<sp-divider size="large" style={{ marginBottom: '10px' }}></sp-divider>
<sp-body>
Two different ways to create modal dialogs.<br />
Please note that Spectrum Dialogs API is still a work in progress.
</sp-body>
<div className="row" style={{ margin: '20px', justifyContent: 'space-around' }}>
<sp-action-button onClick={openPreferences} width="160">
<div slot="icon">
<svg>
<!-- Long SVG things going on here -->
</svg>
</div>
Open Preferences</sp-action-button>
</div>
<sp-detail size="L">Photoshop {uxp.host.version}, {uxp.versions.uxp}</sp-detail>
</div>
);
}
export default Dialogs;
The openPreferences()
function is in charge of opening the dialog: interestingly, it uses ReactDOM.render()
to render the <Preferences>
component, but first it needs to create a <dialog>
element (via createElement()
) stored in the preferencesDialog
variable, and then append it to the <body>
. Only then youāre allowed to uxpShowModal()
. Also note that when the modal is closed thereās no need to keep the <dialog>
around anymore and you can remove()
it.
Nothing relevant to mention in the HTML part, just a very simple Spectrum Component structure with a bit of extra styles to keep things properly spaced.
The Preferences.jsx
is as follows:
import React, { useState } from 'react';
const Preferences = ({ dialog }) => {
const [smartObject, setSmartObject] = useState(true);
const [vectorContent, setVectorContent] = useState(false);
return (
<div className="column">
<sp-heading size="S" style={{marginBottom: '20px'}}>
Set Defaults
</sp-heading>
<div className="column"
style={{ border: 'solid 1px #888',
borderRadius: '4px',
padding: '10px',
marginBottom: '30px' }}>
<sp-checkbox
checked={smartObject ? true : null}
onInput={(evt) => setSmartObject(evt.target.checked) } >
Smart Objects
</sp-checkbox>
<sp-checkbox
checked={vectorContent ? true : null}
onInput={(evt) => setVectorContent(evt.target.checked) }>
Vector Content
</sp-checkbox>
</div>
<div className="row" style={{justifyContent: 'flex-end'}}>
<sp-button variant="secondary"
onClick={() => dialog.close("reasonCanceled")}>
Cancel
</sp-button>
<sp-button variant="primary" style={{marginLeft: '10px'}}
onClick={() => dialog.close({smartObject,vectorContent })}>
Ok
</sp-button>
</div>
</div>
);
}
export default Preferences;
Itās a stateless function component where Iāve used the state
hook to keep track of the two bits of information (the fictitious smartObject
and vectorContent
) that I want to pass along to the panel on close ā see the onClick()
handler for the āOkā button, whereas the āCancelā returns the same "reasonCanceled"
.
As Iāve mentioned in the previous video about Spectrum components, you always must use the ternary operator for checkboxes (and set null
when needed) otherwise they wonāt work properly. Please note that Iām using inline handler functions for the onInput
event just for convenience. Also note that, and this is peculiar to React.js, the style
must be an object ā hence the double curly braces: one set to indicate inline JavaScript, one set for the object notation, with camelCase properties. And finally className
in lieu of class
: but remember, only in standard elements, not Spectrum components that accept the usual class
.
This is just an example; in the real world you may want to, say, always store the values (e.g. in the localStorage
) and pass them to the <Preferences>
component to populate the modal when itās shown ā but the goal here is to focus on dialog creation.
Recap
Dialogs as we knew them (as ScriptUI Windows, or various simple pop-ups like prompt()
and confirm()
) are banned in UXP-land. UXP plugins deal with modeless panels natively, and can display modal dialogs as <dialog>
elements via uxpShowModal()
. <sp-dialogs>
(that are slightly different) are coming in the near future so keep an eye at the official documentation. All in all UXP can manage dialogs quite nicely so the transition to the new system shouldnāt be too big of a deal!
Thanks for following along! If you find this content useful, please consider supporting me: you can either purchase my latest UXP Course with ReactJS, or donate what you can like the following fine people didāitāll be much appreciated! šš»
Thanks to: John Stevenson āļø, Adam Plouff, Dana Frenklach, Dmitry Egorov, Roberto Sabatini, Carlo Diamanti, Wending Dai, Pedro Marques, Anthony Kuyper, Gabriel Correia, Ben Wright, CtrlSoftware, Maiane Araujo, MihĆ”ly DĆ”vid Paseczki, Caspar Shelley.
Stay safe, get the vaccine shot if/when you can ā bye!
The whole series so far
- #01 ā Rundown on the UXP announcement @ the Adobe MAX 2020
- #02 - Documentation
- #03 - UXP Developer Tool
- #04 - Commands vs. Panels and the manifest.json
- #05 - Sync vs. Async code in Photoshop DOM Scripting
- #06 - BatchPlay (part 1): the ActionManager roots
- #07 - BatchPlay (part 2): Alchemist as a UXP Script Listener
- #08 - BatchPlay (part 3): Alchemist as a UXP Inspector
- #09 - Adobe Spectrum UXP
- #10 - Modal Dialogs
- #11 - Flyout Menus and Entrypoints
- #12 - React JS and the UXP plugins Course
- #13 - Manifest v5
- #special - The UXP Landscape Guide