Dialogs
Opening a window from your plugin, the two ways to fill it, and the raw message protocol you need to get a result back.
On this page
Your plugin can open a window on top of the app. Good for settings, pickers and anything step-by-step: things that need more room than a sidebar panel without taking over the screen.
Needs the ui.dialog permission.
await tintero.ui.openDialog({ title: 'Pick a character', width: 600, height: 420 });
openDialog returns as soon as the window exists. It does not wait for anyone to do anything
in it. How you fill it depends on which of the two ways you pick.
Two ways to do it
Which one you get comes down to a single manifest field:
| Shell dialog | File dialog | |
|---|---|---|
| Manifest | no ui.dialog | "ui": { "dialog": "dialog.html" } |
| You fill it with | tintero.ui.render() | the HTML file itself |
| Your code runs in | the plugin’s frame | the dialog’s own frame |
| Good for | small forms, confirmations | anything with real structure |
Shell is less code, but the window is only a surface to paint on: your logic stays in the plugin and you push HTML across to it. File gives the dialog a page and a script of its own, which is what you want the moment there is anything to interact with inside it.
Shell dialog
Nothing extra in the manifest. Open it, then fill it:
plugin.showPicker = async function () {
await tintero.ui.openDialog({ title: 'Pick a character', width: 520, height: 380 });
const characters = await tintero.project.getCharacters();
await tintero.ui.render(
'<style>.row { padding: 6px 10px; cursor: pointer; }</style>' +
characters.map((character) => '<div class="row">' + character.name + '</div>').join(''),
);
};
What you pass gets inserted as HTML. Event handlers you attached back in your own document do not survive the trip, so either use inline handlers or re-render whenever something changes.
File dialog
Point ui.dialog at an HTML file at the top of your plugin folder:
{
"scopes": ["ui.dialog", "project.read.characters"],
"ui": { "dialog": "dialog.html" }
}
<!-- dialog.html -->
<div id="app">Loading…</div>
Files named after it come along automatically, so dialog.html picks up dialog.js and
dialog.css with nothing else to configure. Write it like a very small web page:
// dialog.js
(async function () {
const characters = await tintero.project.getCharacters();
document.getElementById('app').innerHTML = characters
.map((character) => '<div>' + character.name + '</div>')
.join('');
})();
tintero works inside the dialog exactly as it does in the plugin, with the same permissions.
Passing data in
Whatever you put in data reaches the dialog two ways. Take your pick:
await tintero.ui.openDialog({ title: 'Edit', data: { characterId: 'c1' } });
// dialog.js: available synchronously at startup
const initial = window.__DIALOG_INIT_DATA__;
// …or as a message, if you would rather react to it
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'dialog-init') {
render(event.data.data);
}
});
Closing, and getting a result back
tintero.ui.closeDialog() closes your dialog from the plugin’s side.
To close it from inside the dialog and hand something back, post the message Tintero is listening for:
// dialog.js
document.getElementById('save').onclick = function () {
parent.postMessage({ type: 'dialog-close', result: { name: input.value } }, '*');
};
| Message | Direction | What it does |
|---|---|---|
dialog-close | dialog → host | You send this. Closes the window; anything on `result` goes back to the opener. |
dialog-init | host → dialog | Carries whatever you passed as `data` to openDialog(). Also set on window.__DIALOG_INIT_DATA__. |
dialog-opened | host → plugin | The window exists and is showing your content. |
dialog-ready | dialog → host | Sent for you once your dialog scripts have run. The host answers with dialog-init. |
You only ever send dialog-close yourself. The rest the SDK and the host handle
between them. They are listed so that a message arriving in your message listener
is never a mystery.
Opening a second dialog closes the first. One at a time, per plugin.
Sizing
width and height are in pixels. Leave them out and you get
600 × 400. Ask for too much and you get 95vw / 90vh.
On a phone that cap is doing all the work, so build the contents to reflow rather than assuming
you got the size you asked for.
Checklist
- ui.dialog is in
scopes, oropenDialogfails withSCOPE_DENIED. - Your CSS and JavaScript are inline, or named
dialog.cssanddialog.js. - You are not waiting on
openDialogfor an answer. It returns immediately. Usedialog-close. - You remember that
ui.render()goes to the dialog while one is open.