import { ChooseLocalFileItem, saveAs } from './local-storage-drive.js'
import { UploadItem, downloadFile } from './upload-download.js'
import { ImportFromURLItem, loadFromURL } from './load-from-url.js'
import { appSettings } from './settings-install.js'
import { appURL, isValidURL } from './utilities.js'
import { LurchDocument } from './lurch-document.js'
import { syntaxTreeHTML, putdownHTML } from './notation.js'
/**
* This class makes it easier to create and use TinyMCE dialogs that have
* components that work well with Lurch. It handles a lot of the boilerplate
* code that TinyMCE demands and it allows us to work in a more object-oriented
* way, rather than passing around large pieces of JSON and writing all of a
* dialog's code in one big function call.
*/
export class Dialog {
/**
* Create (but do not yet show) a dialog box associated with a given editor.
*
* @param {string} title - the title to display at the top of the dialog
* @param {tinymce.Editor} editor - the editor in which to create the dialog
*/
constructor ( title, editor ) {
this.editor = editor
this.json = {
title,
body : {
type : 'panel',
items : [ ]
},
buttons : [
{
name : 'OK',
text : 'OK',
type : 'submit',
buttonType : 'primary'
},
{
name : 'Cancel',
text : 'Cancel',
type : 'cancel'
}
],
onChange : ( ...args ) => this.onChange?.( ...args ),
onAction : ( ...args ) => {
this.items.forEach( item => item.onAction?.( ...args ) )
},
onSubmit : () => {
this.dialog?.close()
this.resolver?.( true )
},
onCancel : () => {
this.dialog?.close()
this.resolver?.( false )
},
onTabChange : ( _, details ) => {
this.currentTabName = details.newTabName
this.runItemShowHandlers()
}
}
this.dialog = null
this.currentTabName = null
this.items = [ ]
this.focusItem = null
this.hideHeader = false
this.hideFooter = false
}
/**
* Close this dialog box and resolve any open promise with a "false"
* argument, as if the user had canceled the dialog.
*/
close () {
this.dialog?.close()
this.resolver?.( false )
}
/**
* TinyMCE dialogs allow you to specify any set of buttons for the dialog's
* footer. By default, every dialog this class displays will have an OK
* button for submitting the dialog and a Cancel button for canceling it.
* You can replace those buttons by calling this function, passing an array
* of JSON objects, one for each button you wish to define.
*
* For example, to use just an OK button, you might do the following
* (although you could accomplish it more easily with
* {@link Dialog#removeButton removeButton()} instead).
*
* ```js
* myDialog.setButtons( [
* { text : 'OK', type : 'submit', buttonType : 'primary' }
* ] )
* ```
*
* @param {any[]} buttons - the JSON code for the buttons in the dialog's
* footer
* @see {@link Dialog#removeButton removeButton()}
*/
setButtons ( ...buttons ) {
this.json.buttons = buttons
}
/**
* Remove one of the buttons in this dialog. Especially useful if the
* dialog is for informational purposes only, and doesn't need a "cancel"
* button. You could call `myDialog.removeButton( 'Cancel' )`.
*
* @param {string} text - the text shown on the button to be removed
* @see {@link Dialog#setButtons setButtons()}
*/
removeButton ( text ) {
this.json.buttons = this.json.buttons.filter(
button => button.text != text )
}
/**
* If you want to keep a submit button but not have it named with its
* default text of "OK" you can use this function to rename that default
* button. It will still function as a submit button.
*
* @param {string} text - the new text to use in place of "OK"
*/
setOK ( text ) {
const button = this.json.buttons.find( button =>
button.type == 'submit' && button.buttonType == 'primary' )
if ( button ) button.text = text
}
/**
* Any TinyMCE dialog can have an object mapping control IDs to values, to
* fill the dialog's input controls with their initial values. To specify
* that mapping, use this function. Its keys should be the names of any
* input controls you've added to this dialog using
* {@link Dialog#addItem addItem()}.
*
* @param {Object} data - the mapping from control names to values
*/
setInitialData ( data ) {
this.json.initialData = data
}
/**
* Dialogs are not, by default, split into tabs, but are just one large
* panel of controls. You can split them into a set of tabs by calling
* this function and providing the names of the tabs you want created.
*
* @param {string[]} titles - the titles of the tabs
* @see {@link Dialog#currentTabTitle currentTabTitle()}
* @see {@link Dialog#showTab showTab()}
* @see {@link Dialog#removeTabs removeTabs()}
*/
setTabs ( ...titles ) {
this.json.body = {
type : 'tabpanel',
tabs : titles.map( title => {
return {
name : title.replace( /[^a-zA-Z]/g, '' ),
title : title,
items : [ ]
}
} )
}
}
/**
* This is the reverse operation of {@link Dialog#setTabs setTabs()}. It
* removes all tabs, thus collapsing all items onto one pane of the dialog.
* This is almost never needed, because you typically set up a dialog once,
* then use it, and don't need to change its structure in this way.
*
* @see {@link Dialog#setTabs setTabs()}
*/
removeTabs () {
this.json.body = { type : 'panel', items : [ ] }
}
/**
* If you have split your dialog into tabs using the {@link Dialog#setTabs
* setTabs()} function, and shown your dialog, this function will report the
* title shown on the top of the currently visible tab.
*
* @returns {string} the title of the currently shown tab
* @see {@link Dialog#setTabs setTabs()}
* @see {@link Dialog#showTab showTab()}
*/
currentTabTitle () {
const n = this.json.body.tabs ? this.json.body.tabs.length : 0
for ( let i = 0 ; i < n ; i++ )
if ( this.json.body.tabs[i].name == this.currentTabName )
return this.json.body.tabs[i].title
}
/**
* If you have split your dialog into tabs using the {@link Dialog#setTabs
* setTabs()} function, and shown your dialog, you can call this function to
* change which tab is visible, specifying the new one to show by its title.
*
* @param {string} title - the title of the tab you want to switch to
*/
showTab ( title ) {
const n = this.json.body.tabs ? this.json.body.tabs.length : 0
for ( let i = 0 ; i < n ; i++ )
if ( this.json.body.tabs[i].title == title )
return this.dialog.showTab( this.json.body.tabs[i].name )
}
/**
* Add an item to the dialog. There is not a specific class defined for
* dialog items, because they need provide only a `.json()` method that
* creates the JSON code for all the body components the item adds to the
* dialog. However, items can optionally also provide `.onAction()` and
* `.onShow()` event handlers, as well as a `.get()` event that can be more
* flexible than `dialog.getData()['arg']`.
*
* @param {Object} item - the item to add, any object that has a `.json()`
* method that creates JSON code appropriate for TinyMCE dialogs
* @param {string} tabTitle - the tab into which to insert the item, if the
* dialog is split into tabs; leave this blank if it is not
* @see {@link AlertItem}
* @see {@link HTMLItem}
* @see {@link TextInputItem}
* @see {@link ButtonItem}
* @see {@link ImportFromURLItem}
* @see {@link ChooseLocalFileItem}
* @see {@link UploadItem}
* @see {@link SelectBoxItem}
* @see {@link Dialog#removeItem removeItem()}
*/
addItem ( item, tabTitle = null ) {
if ( !this.items.includes( item ) ) {
this.items.push( item )
item.dialog = this
}
item._generated_json = item.json()
if ( !tabTitle )
return item._generated_json.forEach(
item => this.json.body.items.push( item ) )
const tab = this.json.body.tabs.find( tab => tab.title == tabTitle )
if ( !tab )
throw new Error( `Invalid tab: ${tabTitle}` )
item.tab = tab.name
item._generated_json.forEach( item => tab.items.push( item ) )
}
/**
* Remove an item from the dialog, and also remove from the dialog's
* internal JSON data structure any JSON items created from the item that
* is being removed.
*
* @param {integer} index - the index of the item to remove
* @see {@link Dialog#addItem addItem()}
*/
removeItem ( index ) {
if ( this.json.body.tabs ) {
const tab = this.json.body.tabs.find(
tab => tab.name == this.items[index].tab )
tab.items = tab.items.filter( item =>
!this.items[index]._generated_json.includes( item ) )
} else {
this.json.body.items = this.json.body.items.filter( item =>
!this.items[index]._generated_json.includes( item ) )
}
this.items.splice( index, 1 )
}
/**
* Show the dialog, run any `.onShow()` event handlers in any items in the
* dialog, and return a promise. That promise resolves when the user
* submits or cancels the dialog, and returns true if it was a submit and
* false if it was a cancel.
*
* @returns {Promise} a promise that resolves when the dialog closes or
* encounters an error
*/
show () {
if ( this.json.body.tabs )
this.currentTabName = this.json.body.tabs[0].name
this.dialog = this.editor.windowManager.open( this.json )
this.element = Dialog.getTopDialogElement()
if ( this.hideHeader )
this.querySelector( '.tox-dialog__header' ).style.display = 'none'
if ( this.hideFooter )
this.querySelector( '.tox-dialog__footer' ).style.display = 'none'
return new Promise( ( resolve, reject ) => {
this.resolver = resolve
this.rejecter = reject
this.runItemShowHandlers()
} ).finally( () => {
delete this.element
} )
}
/**
* Specify which control should receive focus when the dialog is first
* shown. You can pass `null` to clear this setting.
*
* @param {string} name - the name of the control to focus when the dialog
* is shown
*/
setDefaultFocus ( name ) { this.focusItem = name }
// For internal use only; see show().
runItemShowHandlers () {
if ( this.json.body.tabs )
this.items.forEach( item => {
if ( item.tab == this.currentTabName )
item.onShow?.( this.dialog )
} )
else
this.items.forEach( item => item.onShow?.( this.dialog ) )
if ( this.focusItem )
setTimeout( () => this.dialog.focus( this.focusItem ) )
}
/**
* This is similar to TinyMCE's `dialog.getData()[key]` syntax, but more
* flexible in that before resorting to that fallback method, it first asks
* each item in the dialog if it wants to return a value for the given key.
* This lets those items behave in a more sophisticated and flexible manner
* if they choose to. If none of them returns a value different from
* `undefined`, then we fall back on the standard TinyMCE method shown
* above. If the dialog is not shown, this returns undefined.
*
* @param {string} key - the key whose value should be looked up
* @returns {any} the corresponding value
*/
get ( key ) {
if ( !this.dialog ) return undefined
const data = this.dialog.getData()
for ( let i = 0 ; i < this.items.length ; i++ ) {
if ( !this.items[i].get ) continue
const maybe = this.items[i].get( key, data )
if ( maybe !== undefined ) return maybe
}
return data[key]
}
/**
* Ask TinyMCE to regenerate the dialog's content from its JSON encoding.
*/
reload () {
this.dialog?.redial( this.json )
this.runItemShowHandlers()
}
/**
* Behaves exactly like `document.querySelector()`, except that it is run on
* just the element representing this dialog box, and thus will return only
* an element that appears in this dialog. Or, if this dialog has no
* element matching the given selector, it returns undefined.
*
* @param {string} selector - the CSS selector to use for the query
* @returns {HTMLElement} the first element that matches the selector
* @see {@link Dialog#querySelectorAll querySelectorAll()}
* @see {@link Dialog.getTopDialogElement getTopDialogElement()}
*/
querySelector ( selector ) {
return this.element?.querySelector( selector )
}
/**
* Behaves exactly like `document.querySelectorAll()`, except that it is run
* on just the element representing this dialog box, and thus will return
* only elements that appear in this dialog. Or, if this dialog has no
* element matching the given selector, it returns an empty node list.
*
* @param {string} selector - the CSS selector to use for the query
* @returns {NodeList} the list of elements that match the selector
* @see {@link Dialog#querySelector querySelector()}
* @see {@link Dialog.getTopDialogElement getTopDialogElement()}
*/
querySelectorAll ( selector ) {
return this.element?.querySelectorAll( selector )
}
/**
* TinyMCE may have open zero or more dialog boxes at any given time. This
* method returns the HTML element (a DIV) corresponding to the topmost
* dialog box, or undefined if there are no open dialogs.
*
* Whenever an instance of this class is placed on screen using its
* {@link Dialog#show show()} method, this method is used to notice which
* DOM element represents that dialog, and it is stored in the dialog's
* `element` field for later use by functions like
* {@link Dialog#querySelector querySelector()} and
* {@link Dialog#querySelectorAll querySelectorAll()}.
*
* @returns {HTMLDivElement} the DIV element in the DOM representing the
* topmost TinyMCE dialog
*/
static getTopDialogElement () {
const allDialogElements =
document.querySelectorAll( 'div.tox-dialog[role="dialog"]' )
return allDialogElements[allDialogElements.length - 1]
}
/**
* A static method that creates a dialog with just an OK button and a
* success message in it. This is a convenience method that makes it
* possible to show a success message in a modal dialog using just one line
* of code.
*
* @param {tinymce.Editor} editor - the editor over which to show the dialog
* @param {string} text - the success message to be displayed
* @param {string} [title="Success"] - an optional title for the dialog
* @returns {Promise} a promise that resolves when the dialog closes, the
* result of a call to {@link Dialog#show show()}
* @see {@link Dialog.failure Dialog.failure()}
* @see {@link Dialog.areYouSure Dialog.areYouSure()}
*/
static success ( editor, text, title = 'Success' ) {
const dialog = new Dialog( title, editor )
dialog.removeButton( 'Cancel' )
dialog.addItem( new AlertItem( 'success', text ) )
return dialog.show()
}
/**
* A static method that creates a dialog with just an OK button and a
* failure message in it. This is a convenience method that makes it
* possible to show a failure message in a modal dialog using just one line
* of code.
*
* @param {tinymce.Editor} editor - the editor over which to show the dialog
* @param {string} text - the failure message to be displayed
* @param {string} [title="Failure"] - an optional title for the dialog
* @returns {Promise} a promise that resolves when the dialog closes, the
* result of a call to {@link Dialog#show show()}
* @see {@link Dialog.success Dialog.success()}
* @see {@link Dialog.areYouSure Dialog.areYouSure()}
*/
static failure ( editor, text, title = 'Failure' ) {
const dialog = new Dialog( title, editor )
dialog.removeButton( 'Cancel' )
dialog.addItem( new AlertItem( 'error', text ) )
return dialog.show()
}
/**
* A static method that creates a dialog entitled "Are you sure?" and
* prompting the user with a warning containing the given text. The return
* value is a promise that resolves with a boolean argument indicating
* whether the user clicked yes/I'm sure (true) or no/Cancel (false).
*
* @param {tinymce.Editor} editor - the editor over which to show the dialog
* @param {string} text - the question to be displayed
* @returns {Promise} a promise that resolves when the dialog closes, the
* result of a call to {@link Dialog#show show()}
* @see {@link Dialog.success Dialog.success()}
* @see {@link Dialog.failure Dialog.failure()}
*/
static areYouSure ( editor, text ) {
const dialog = new Dialog( 'Are you sure?', editor )
dialog.addItem( new AlertItem( 'warn', text ) )
dialog.setOK( 'I\'m sure' )
return dialog.show()
}
/**
* This static function shows a dialog for loading a file. The dialog
* contains three tabs, one for loading a file from browser storage, one for
* letting the user upload from their computer, and one for letting the user
* import a file from a web URL. The user can choose their preferred method
* and import the file.
*
* The function returns a promise that resolves when the user closes the
* dialog. If the user canceled and did not choose or accept a file, the
* handler will be passed no arguments. If the user did choose and accept a
* file, the argument passed to the handler will be an object with two
* fields, of the form `{ filename : '...', content : '...' }`, where the
* filename will be the filename or URL and the content will be the entirety
* of the file's content, which may be large. Both are strings.
*
* @param {tinymce.Editor} editor - the editor instance in which to display
* the dialog
* @param {string} [title="File"] - the title to show at the top of the dialog
* @returns {Promise} a promise that resolves to the file information as
* specified above, or rejects if an error occurs
* @see {@link Dialog.saveFile Dialog.saveFile()}
*/
static loadFile ( editor, title = 'File' ) {
const dialog = new Dialog( title, editor )
dialog.json.size = 'medium'
const tabNames = editor.appOptions.fileOpenTabs || [
'From browser storage', 'From your computer', 'From the web'
]
dialog.setTabs( ...tabNames )
if ( tabNames.includes( 'From browser storage' ) )
dialog.addItem( new ChooseLocalFileItem( 'localFile' ), 'From browser storage' )
if ( tabNames.includes( 'From your computer' ) )
dialog.addItem( new UploadItem( 'uploadedFile' ), 'From your computer' )
if ( tabNames.includes( 'From the web' ) )
dialog.addItem( new ImportFromURLItem( 'importedFile' ), 'From the web' )
return new Promise( ( resolve, reject ) => {
dialog.show().then( userHitOK => {
if ( !userHitOK ) return resolve()
const title = dialog.currentTabTitle()
if ( title == 'From browser storage' ) {
resolve( dialog.get( 'localFile' ) )
} else if ( title == 'From your computer' ) {
resolve( dialog.get( 'uploadedFile' ) )
} else if ( title == 'From the web' ) {
const url = new URL( dialog.get( 'importedFile' ), appURL() ).href
loadFromURL( url )
.then( content => {
resolve( {
filename : url,
content : content,
source : 'web'
} )
} ).catch( () => {
Dialog.notify( editor, 'error',
`Unable to open a document from ${url}` )
reject( 'Could not download from URL.' )
} )
} else {
reject( 'Unknown tab: ' + title )
}
} ).catch( reject )
setTimeout( () => {
const defaultTab = appSettings.get( 'default open dialog tab' )
if ( tabNames.includes( defaultTab ) )
dialog.showTab( defaultTab )
} )
} )
}
/**
* This static function shows a dialog for saving a file. The dialog
* contains two tabs, one for saving a file to browser storage and one for
* letting the user download to their computer. The user can choose their
* preferred method and save the file.
*
* The function returns a promise that resolves when the user closes the
* dialog. If the user canceled and did not choose or accept a file, the
* handler will be passed a boolean false. If the downloaded the file, the
* handler will be passed a boolean true. If the user saved the file to the
* browser's local storage, the handler will be passed a string containing
* the chosen filename.
*
* @param {tinymce.Editor} editor - the editor instance in which to display
* the dialog
* @param {string} [title="File"] - the title to show at the top of the dialog
* @returns {Promise} a promise that resolves as specified above, or rejects
* if an error occurs
* @see {@link Dialog.loadFile Dialog.loadFile()}
*/
static saveFile ( editor, title = 'File' ) {
const dialog = new Dialog( title, editor )
const tabNames = editor.appOptions.fileSaveTabs || [
'To browser storage', 'To your computer'
]
dialog.setTabs( ...tabNames )
if ( tabNames.includes( 'To browser storage' ) ) {
dialog.addItem( new TextInputItem( 'filename', 'Filename' ),
'To browser storage' )
dialog.setDefaultFocus( 'filename' )
}
if ( tabNames.includes( 'To your computer' ) ) {
dialog.addItem( new TextInputItem( 'downloadFilename', 'Filename' ),
'To your computer' )
dialog.addItem( new HTMLItem( `
<p>Clicking OK below will download the current Lurch document to
your computer as an HTML file.</p>
` ), 'To your computer' )
}
const LD = new LurchDocument( editor )
let filename = LD.getFileID()
if ( filename && filename.startsWith( 'file:///' ) ) {
dialog.setInitialData( {
downloadFilename : filename.substring( 8 )
} )
} else if ( filename && !isValidURL( filename ) ) {
dialog.setInitialData( { filename } )
}
dialog.json.size = 'medium'
return new Promise( ( resolve, reject ) => {
dialog.show().then( userHitOK => {
if ( !userHitOK ) return resolve( false )
const title = dialog.currentTabTitle()
if ( title == 'To browser storage' ) {
const filename = dialog.get( 'filename' )
saveAs( editor, filename )
resolve( filename )
} else if ( title == 'To your computer' ) {
LD.setFileID( `file:///${ dialog.get( 'downloadFilename' ) }` )
downloadFile( editor )
resolve( true )
} else {
reject( 'Unknown tab: ' + title )
}
} ).catch( reject )
setTimeout( () => {
const defaultTab = appSettings.get( 'default save dialog tab' )
if ( tabNames.includes( defaultTab ) )
dialog.showTab( defaultTab )
} )
} )
}
/**
* Pop up a notification over the editor. This can be done with TinyMCE's
* `NotificationManager` class, but this function just makes it slightly
* more convenient, because the parameters are named, and one does not need
* to remember the JSON encoding of them.
*
* @param {tinymce.Editor} editor - the editor in which to show the
* notification
* @param {string} type - the type of notification to show, which TinyMCE
* requires must be one of "success", "info", "warning", or "error"
* @param {string} text - the content of the notification
* @param {integer} timeout - how long (in ms) until the notification
* disappears (optional; defaults to 2000 for success notifications and
* no timeout for everything else)
*/
static notify ( editor, type, text, timeout ) {
if ( type == 'success' )
timeout ||= 2000
editor.notificationManager.open( {
type : type,
text : text,
timeout : timeout
} )
}
/**
* Show the meaning of any atom in the editor in a dialog, as long as the
* atom has a meaning as a LogicConcept. This will use the representations
* defined in {@link module:Notation.syntaxTreeHTML syntaxTreeHTML()} and
* {@link module:Notation.putdownHTML putdownHTML()}, each in a separate tab
* of the dialog.
*
* @param {Atom} atom - the Atom whose meaning should be shown
*/
static meaningOfAtom ( atom ) {
const result = new Dialog( 'Meaning', atom.editor )
result.removeButton( 'Cancel' )
result.setTabs( 'Hierarchy', 'Code' )
const LCs = atom.toLCs()
result.addItem( new HTMLItem( syntaxTreeHTML( LCs ) ), 'Hierarchy' )
result.addItem( new HTMLItem( putdownHTML( LCs ) ), 'Code' )
result.show()
setTimeout( () =>
result.showTab( appSettings.get( 'preferred meaning style' ) ) )
}
}
/**
* An item that can be used in a {@link Dialog} to represent an alert (that is,
* a piece of text that is colored and given an icon to make it more obvious).
* This corresponds to the "alertbanner" type of body component in a TinyMCE
* dialog.
*/
export class AlertItem {
/**
* Construct an alert item. An appropriate icon is chosen to correspond to
* the type of alert constructed.
*
* @param {string} type - the type of alert, one of "info", "warn", "error",
* or "success"
* @param {string} text - the text to show in the alert
*/
constructor ( type, text ) {
this.type = type
this.text = text
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ {
type : 'alertbanner',
text : this.text,
level : this.type,
icon : this.level == 'success' ? 'selected' :
this.level == 'warn' ? 'warning' :
this.level == 'info' ? 'info' : 'notice'
} ]
}
}
/**
* An item that can be used in a {@link Dialog} to represent any HTML content.
* This corresponds to the "htmlpanel" type of body component in a TinyMCE
* dialog.
*/
export class HTMLItem {
/**
* Construct an HTML item.
*
* @param {string} html - the HTML code of the content to show
*/
constructor ( html ) {
this.html = html
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ { type : 'htmlpanel', html : this.html } ]
}
}
/**
* An item that can be used in a {@link Dialog} to represent a blank text box
* into which the user can type content. This corresponds to the "input" type
* of body component in a TinyMCE dialog.
*/
export class TextInputItem {
/**
* Construct a new text input control.
*
* @param {string} name - the key to use to identify this input control's
* content in the dialog's key-value mapping for all input controls
* @param {string} label - the text to place next to the input control to
* explain it to the user
* @param {string} placeholder - optional, the text to include inside the
* control when it is blank, as an example
*/
constructor ( name, label, placeholder ) {
this.name = name
this.label = label
this.placeholder = placeholder
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
const result = {
type : 'input',
name : this.name,
label : this.label
}
if ( this.placeholder )
result.placeholder = this.placeholder
return [ result ]
}
}
/**
* Functions just like {@link TextInputItem}, except it is an HTML textarea,
* and thus can contain multiple lines of input text.
*/
export class LongTextInputItem {
/**
* Construct a new long text input control.
*
* @param {string} name - the key to use to identify this input control's
* content in the dialog's key-value mapping for all input controls
* @param {string} label - the text to place above the input control to
* explain it to the user
* @param {string} placeholder - optional, the text to include inside the
* control when it is blank, as an example
*/
constructor ( name, label, placeholder ) {
this.name = name
this.label = label
this.placeholder = placeholder
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
const result = {
type : 'textarea',
name : this.name,
label : this.label
}
if ( this.placeholder )
result.placeholder = this.placeholder
return [ result ]
}
}
/**
* An item that can be used in a {@link Dialog} and shows up as a clickable
* button. This corresponds to the "button" type of body component in a TinyMCE
* dialog.
*/
export class ButtonItem {
/**
* Construct a button.
*
* @param {string} text - the text shown on the button
* @param {function} action - the function to call when the button is
* clicked; it will be passed the {@link Dialog} instance
* @param [string] name - the name of the button, internally (will default
* to the text parameter, but if you have multiple buttons with the same
* text, TinyMCE requires they all have different names internally)
*/
constructor ( text, action, name ) {
this.text = text
this.action = action
this.name = ( name || text ).replace( /[^_a-zA-Z0-9]/g, '_' )
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ {
type : 'button',
name : this.name,
text : this.text,
maximized : true
} ]
}
// internal use only; calls this button's action if it is pressed
onAction ( dialog, details ) {
if ( details.name == this.name ) this.action( dialog )
}
}
/**
* An item that can be used in a {@link Dialog} and shows up as a dropdown list
* of options. This corresponds to the "selectbox" type of body component in
* a TinyMCE dialog.
*/
export class SelectBoxItem {
/**
* Construct a select box.
*
* @param {string} name - the name of the control in the dialog, used for
* querying its value when the dialog closes, or providing an initial
* value when the dialog opens
* @param {string} label - the label to show next to the select box in the
* user interface
* @param {string[]} items - the array of items to be shown in the select
* box in the user interface
*/
constructor ( name, label, items ) {
this.name = name
this.label = label
this.items = items
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ {
type : 'selectbox',
name : this.name,
label : this.label,
items : this.items.map( name => {
return { value : name, text : name }
} )
} ]
}
}
/**
* An item that can be used in a {@link Dialog} and shows up as a checkbox.
* This corresponds to the "checkbox" type of body component in a TinyMCE
* dialog.
*/
export class CheckBoxItem {
/**
* Construct a checkbox.
*
* @param {string} name - the name of the control in the dialog, used for
* querying its value when the dialog closes, or providing an initial
* value when the dialog opens
* @param {string} label - the label to show next to the checkbox in the
* user interface
*/
constructor ( name, label ) {
this.name = name
this.label = label
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ {
type : 'checkbox',
name : this.name,
label : this.label
} ]
}
}
/**
* An item that can be used in a {@link Dialog} and takes up the full width of
* the dialog, but can be populated with an array of inner items, that will be
* arranged in that row. This corresponds to the "bar" type of body component
* in a TinyMCE dialog. By default, all columns in the row take up the same
* amount of space, but you can make one larger if you modify the style of the
* element after it has been placed into the DOM, setting one of the
* sub-elements to have `width: 100%`.
*/
export class DialogRow {
/**
* Construct a dialog row.
*
* @param {...Object} items - an array of dialog items (e.g.,
* {@link AlertItem} or {@link ButtonItem}) to place into this row
*/
constructor ( ...items ) {
this.items = items
}
// internal use only; creates the JSON to represent this object to TinyMCE
json () {
return [ {
type : 'bar',
items : this.items.map( item => item.json() ).flat()
} ]
}
// internal use only; pass any action notifications on to my children
onAction ( ...args ) {
this.items.forEach( item => item.onAction?.( ...args ) )
}
}
source