Are you interested in converting your plugin's modal dialog box UI into a panel UI? Keep in mind that not every modal dialog flow is a good fit for this change. If you are curious about what differences exist, take a look at the docs for each UI entrypoint:
If you determine your modal-based plugin would be even better as a panel, read on!
- Basic knowledge of HTML, CSS, and JavaScript
- Modal Quick Start Tutorial
- Panel Quick Start Tutorial
- Debugging Tutorial
The complete example can be found in our Samples repository.
We are going to take a look at a sample plugin that lets users increase the width and height of the selected rectangle by inputting pixel values in the UI. As you can imagine, this process would be more efficient for the user if the plugin provided UI in a panel.
Let's see what this plugin will look like before and after:
As a modal
As a panel
Now, let's go through the steps of converting a modal plugin to a panel-based plugin.
Note that your plugin structure does not change. Just like with modal plugins, the minimum required files for your panel plugin to work are main.js
and manifest.json
.
You might have more files than this, depending on how you have structured your plugin.
Let's take a look at both cases.
Modal:
"host": {
"app": "XD",
"minVersion": "15.0"
},
"uiEntryPoints": [
{
"type": "menu",
"label": "Enlarge a Rectangle",
"commandId": "enlargeRectangle"
}
]
Panel:
"host": {
"app": "XD",
"minVersion": "21.0"
},
"uiEntryPoints": [
{
"type": "panel",
"label": "Enlarge a Rectangle",
"panelId": "enlargeRectangle"
}
]
As you can see, host.minVersion
is changed to 21.0
since 21.0
is the earliest version of XD that supports panels. Also, uiEntryPoints
needs to be updated as well. The type
key should be changed to panel
and commandId
key needs to be changed to panelId
.
A modal-dialog command needs one function to be exported:
function enlargeRectangle() {
// your code here
}
module.exports = {
commands: {
enlargeRectangle
}
};
Plugin panel UI, on the other hand, requires you to export an object with one required lifecycle method named show
, and with optional lifecycle methods name hide
and update
:
function show(event) {
// your code here
}
function hide(event) {
// your code here
}
function update(selection) {
// your code here
}
module.exports = {
panels: {
enlargeRectangle: {
show,
hide,
update
}
}
};
Review the specifics of these panel lifecycle methods in the panel-object API reference before moving on.
Let's first look at the main.js
file of your original modal-dialog plugin:
const { selection } = require("scenegraph"); // [1]
let dialog;
function enlargeRectangle() { // [2]
const html = `
<style>
.break {
flex-wrap: wrap;
}
label.row > span {
color: #8E8E8E;
width: 20px;
text-align: right;
font-size: 9px;
}
label.row input {
flex: 1 1 auto;
}
form {
width:90%;
margin: -20px;
padding: 0px;
}
</style>
<form method="dialog" id="main">
<div class="row break">
<label class="row">
<span>↕︎</span>
<input type="number" uxp-quiet="true" id="txtV" value="10" placeholder="Height" />
</label>
<label class="row">
<span>↔︎</span>
<input type="number" uxp-quiet="true" id="txtH" value="10" placeholder="Width" />
</label>
</div>
<footer>
<button id="ok" type="submit" uxp-variant="cta">Apply</button>
</footer>
</form>
`;
function increaseRectangleSize() { // [3]
const height = Number(document.querySelector("#txtV").value); // [4]
const width = Number(document.querySelector("#txtH").value);
const selectedRectangle = selection.items[0]; // [5]
selectedRectangle.width += width; // [6]
selectedRectangle.height += height;
}
if (!dialog) {
let dialog = document.createElement("dialog"); // [7]
dialog.innerHTML = html; // [8]
document.appendChild(dialog); // [9]
document.querySelector("form").addEventListener("submit", increaseRectangleSize); // [10]
}
return dialog.showModal(); // [11]
}
module.exports = { // [12]
commands: {
enlargeRectangle
}
};
This code does the following:
- Gets reference to the
selection
. - Creates a
const
calledhtml
and stores your UI markup, including elements forstyle
,form
, and so on. Theform
tag contains adiv
which includes two text input fields and afooter
which has a button for users to click on. Thep
tag contains warning text which is used to warn users when they select a non-rectangle node inside the active XD document. - Creates a nested function called
increaseRectangleSize
. - Gets user input value from the "height" and "width" input elements.
- Gets the first currently selected node. (Some UI logic will be added later to ensure this is a rectangle.)
- Modifies the
width
andheight
of the selected rectangle. - Creates a
dialog
element. - Attaches the HTML created in step #1.
- Attaches the
dialog
element created in step #8 to the DOM. - Adds a submit listener for the form element, attaching the
increaseRectangleSize
function created in step #2. - Returns a JavaScript Promise which will resolve when the dialog is dismissed by the user.
- Exports the main function.
Let's start modifying code in the main.js
file.
Your HTML markup does not have to change, but let's add a p
tag to show a warning message if the user has selected a node that's not a rectangle:
<p id="warning">
This plugin requires you to select a rectangle in the document. Please select
a rectangle.
</p>
Unlike modal UI, panel UI can stay open and persistent, optionally updating in real time based on a user's selection.
Unlike menu commands, which have just a single entry point the user can trigger to run the command, panel plugins have many different ways that the user can interact with their DOM elements to trigger an action. As a result, your panel UI event handlers must call special function when you want to make changes to the XD document.
Any part of your panel that makes changes to the scenegraph needs to be wrapped in an application.editDocument
call:
const { editDocument } = require("application");
editDocument({ editLabel: "Increase rectangle size" }, function(selection) {
const selectedRectangle = selection.items[0];
selectedRectangle.width += width;
selectedRectangle.height += height;
});
Now, instead of creating a dialog
, let's create a panel
element, insert the HTML, and add an event listener with the increaseRectangleSize
function attached.
You can replace these lines of code:
let dialog = document.createElement("dialog");
dialog.innerHTML = html;
document.appendChild(dialog);
document.querySelector("form").addEventListener("submit", increaseRectangleSize);
return dialog.showModal();
... with this:
let panelContainer = document.createElement("panel");
panelContainer.innerHTML = html;
panelContainer.querySelector("form").addEventListener("submit", increaseRectangleSize);
event.node.appendChild(panelContainer);
If you're wondering where the event
object in that last line came from, we'll have a look at that in the next section.
All of the main.js
code we've modified so far will be the content of your show
lifecycle method that runs when the plugin opens.
Note that the show
and hide
lifecycle methods for panel-based plugins receive an event
argument. The event.node
property is the DOM element where you can attach your panel UI for display in the application.
Here is our complete show
method for this plugin:
let panel;
function show(event) {
const html = `
<style>
label.row > span {
color: #8E8E8E;
width: 20px;
text-align: right;
font-size: 9px;
}
label.row input {
flex: 1 1 auto;
}
.show {
display: block;
}
.hide {
display: none;
}
</style>
<form method="dialog" id="main">
<div class="row break">
<label class="row">
<span>↕︎</span>
<input type="number" uxp-quiet="true" id="txtV" value="10" placeholder="Height" />
</label>
<label class="row">
<span>↔︎</span>
<input type="number" uxp-quiet="true" id="txtH" value="10" placeholder="Width" />
</label>
</div>
<footer><button id="ok" type="submit" uxp-variant="cta">Apply</button></footer>
</form>
<p id="warning">This plugin requires you to select a rectangle in the document. Please select a rectangle.</p>
`;
function increaseRectangleSize() {
const { editDocument } = require("application");
const height = Number(document.querySelector("#txtV").value);
const width = Number(document.querySelector("#txtH").value);
editDocument({ editLabel: "Increase rectangle size" }, function(selection) {
const selectedRectangle = selection.items[0];
selectedRectangle.width += width;
selectedRectangle.height += height;
});
}
if (!panel) {
panel = document.createElement("div");
panel.innerHTML = HTML;
panel.querySelector("form").addEventListener("submit", increaseRectangleSize);
event.node.appendChild(panel);
}
}
Next, let's add the optional hide
lifecycle method, which runs when the panel UI becomes invisible:
function hide(event) {
// This function triggers when the panel is hidden by user
}
The update
function is another optional lifecycle method that allows your plugin to dynamically respond to the user's selection changes.
We'll break this code down below:
function update(selection) { // [1]
const { Rectangle } = require("scenegraph"); // [2]
const form = document.querySelector("form"); // [3]
const warning = document.querySelector("#warning"); // [4]
if (!selection || !(selection.items[0] instanceof Rectangle)) { // [5]
form.className = "hide";
warning.className = "show";
} else {
form.className = "show";
warning.className = "hide";
}
}
This code does the following:
- Uses the
selection
argument.update
provides two arguments,selection
androot
. This example only usesselection
. - Gets reference to the
Rectangle
object imported from thescenegraph
module. - Gets reference to the
form
element in your HTML. - Gets reference to the
p
element with the warning message. - Checks if the user has selected anything and if the selection is a rectangle. If this validation passes, the form appears and the warning message is not shown. If not, the warning message is shown to the user and the form disappears.
Now you have successfully converted your modal plugin to a panel plugin!
Ready to explore further? Take a look at our other resources: