Source

client/ui/elements/dialog.js

/**
 * 
 * The Dialog box is just a Panel with pre-defined items:
 * - OK button
 * - Cancel button, except when dialog type = "message"
 * - Field to input a value if type = "text", "textarea", "select", or "directory"
 * - Clicking on the OK button triggers the specified action.
 * - Clicking on the Cancel button close the dialog.
 * 
 * @param {object|string} config - Configuration object, or a simple text to display in the dialog box
 * @param {string} [config.id] - optional id in case you need to manage this dialog by id
 * @param {string} config.type - dialog | message | danger | text | textarea | select | directory. Default = "dialog"
 * @param {string} [config.defaultValue] - Default value for input or select type
 * @param {string} config.message
 * @param {string} config.textAlign - use "center" to center the text in the dialog box
 * @param {function} config.action - Function called if the user clicks on the OK button. The function receives the input value if the dialog collects a value.
 * @param {object[]} [config.options] - Only for "select" type: define the list of options. 
 * @param {boolean} [config.multiple] - Only for "select" type: allow to select multiple options.
 * @param {boolean} [config.allowValuesNotInList] - Only for "select" type: allow to enter values not in the predefined list of options. Default is false.
 * @param {boolean} [config.users] - Only for "directory" type: allow to select users
 * @param {boolean} [config.groups] - Only for "directory" type: allow to select groups
 * @param {boolean} [config.roles] - Only for "directory" type: allow to select roles
 * @param {boolean} [config.autoClose] - if true (default), the window is closed on validation. If false, the window is closed only if the <action> function returns true.
 * @param {string} [config.icon] - Header icon
 * @param {string} [config.header] - set to false to hide the header
 * @param {string} [config.headerHeight]
 * @param {string} [config.headerColor]
 * @param {string} [config.headerBackgroundColor]
 * @param {string} [config.title]
 * @param {string} [config.buttonOKPosition] - "left" | "right" (default = "right")
 * @param {string} [config.buttonOKText] - Text of the "OK" button. Default = "OK"
 * @param {string} [config.buttonCancelText] - Text of the "Cancel" button. Default = "Cancel"
 * @param {string|boolean} [config.iconOK] - Icon of the "OK" button, or false to hide the icon
 * @param {string|boolean} [config.iconCancel] - Icon of the "Cancel" button, or false to hide the icon
 * @param {string} [config.colorOK] - Hexa color code of the OK button
 * @param {string} [config.colorCancel] - Hexa color code of the Cancel button
 * @param {string} [config.noOK] - If true, hide the OK button
 * @param {string} [config.noCancel] - If true, hide the Cancel button
 * @param {string|number} [config.top]
 * @param {string|number} [config.left]
 * @param {string|number} [config.width]
 * @param {string|number} [config.height]
 * @param {string} [config.animation] - Check [component's animations](https://kissjs.net/doc/out/kiss.ui.component#setAnimation)
 * @param {string|boolean} [backdropFilter] - Add a backdrop filter to the mask
 * @param {number} [zIndex] - z-index of the dialog box (default = 1)
 * @returns this
 * 
 * @example
 * // Display a simple message box: it only has an OK button
 * createDialog({
 *  message: "Your asset is ready"
 * })
 * 
 * // Same thing, using just a text as argument
 * createDialog("Your asset is ready")
 * 
 * // Display a dialog box: it has OK and Cancel buttons
 * createDialog({
 *  type: "dialog",
 *  message: "Do you want to do that?",
 *  action: () => console.log("You've done that!")
 * })
 * 
 * // Display a danger box: OK button and header are red + exclamation icon
 * createDialog({
 *  type: "danger",
 *  message: "Do you want to delete the database?",
 *  action: () => console.log("You've deleted the database!")
 * })
 * 
 * // Display a dialog box with an input field: the callback catches the entered value
 * createDialog({
 *  type: "text",
 *  message: "Please enter your name:",
 *  action: (enteredValue) => console.log("You've entered " + enteredValue)
 * })
 * 
 * // Display a dialog box with a select field: the callback catches the selected values in an array
 * createDialog({
 *  type: "select",
 *  message: "Please select your items:",
 *  multiple: true, // Allow to select multiple options
 *  options: ["Item A", "Item B", "Item C"],
 *  action: (enteredValues) => console.log("You've entered " + enteredValues.join(" / "))
 * })
 */
kiss.ui.Dialog = class Dialog {
	/**
	 * You can create a Dialog using the class or using the shorthand:
	 * ```
	 * // Using kiss namespace
	 * new kiss.ui.Dialog(config)
	 * 
	 * // Using the class
	 * new Dialog(config)
	 * 
	 * // Using the shorthand
	 * createDialog(config)
	 * ```
	 * 
	 * @param {object} config 
	 * @returns this
	 */
	constructor(config) {
		const isMobile = kiss.screen.isMobile
		const dialogId = config.id || kiss.tools.shortUid()
        
		if (typeof config == "string") {
			config = {
				type: "message",
				message: config
			}
		}

		const dialogType = config.type || "dialog"

		if (config.textAlign == "center") {
			config.message = `<center>${config.message}</center>`
		}

		config.message = (config.message || "").replaceAll("\n", "<br>")

		if (config.type == "danger") {
			config.icon = "fas fa-exclamation-triangle"
			config.headerColor = "var(--red)"
			config.headerBackgroundColor = "var(--body-background)"
			config.colorOK = "var(--red)"
		}
		else {
			config.headerColor = "var(--body)"
			config.headerBackgroundColor = "var(--body-background)"
		}

		// OK button
		const buttonOK = {
			hidden: (config.noOK == true),

			type: "button",
			class: (config.type != "danger") ? "button-ok" : "",
			text: config.buttonOKText || txtTitleCase("ok"),
			icon: (config.iconOK === false) ? false : (config.iconOK || "fas fa-check"),
			color: (config.type == "danger") ? "white" : (config.colorOK || null),
			iconColor: (config.type == "danger") ? "white" : (config.colorOK || null),
			backgroundColor: (config.type == "danger") ? "var(--red)" : (config.backgroundColor || null),
			fontWeight: "bold",
			flex: 1,
			styles: {
				"this": (config.colorOK) ? "border-color: " + config.colorOK : ""
			},
			events: {
				click: async function () {
					$(dialogId).validate()
				}
			}
		}

		// CANCEL button
		const buttonCancel = {
			hidden: (dialogType == "message" || config.noCancel == true),

			type: "button",
			class: (config.type != "danger") ? "button-cancel" : "",
			text: config.buttonCancelText || txtTitleCase("cancel"),
			icon: (config.iconCancel === false) ? false : (config.iconCancel || "fas fa-times"),
			iconColor: config.colorCancel || null,
			fontWeight: "bold",
			flex: 1,
			styles: {
				"this": (config.colorCancel) ? "border-color: " + config.colorCancel : ""
			},
			events: {
				click: function () {
					this.closest("a-panel").close()
				}
			}
		}

		// Dialog box
		let dialogConfig = {
			id: dialogId,
			class: "panel-dialog",
			top: config.top || null,
			left: config.left || null,
			width: (isMobile) ? "100%" : (config.width || null),
			height: (isMobile) ? "100%" : (config.height || null),
			borderRadius: (isMobile) ? "0 0 0 0" : "",
			align: "center",
			verticalAlign: "center",
			padding: "2rem",

			modal: true,
			backdropFilter: config.backdropFilter,
			draggable: true,
			closable: !(config.closable === false),
			animation: config.animation || null,
			zIndex: config.zIndex || 1,

			header: (config.header !== false),
			title: config.title || "",
			icon: config.icon || "fas fa-info-circle",
			headerHeight: config.headerHeight,
			headerColor: config.headerColor || "var(--panel-header)",
			headerBackgroundColor: config.headerBackgroundColor || "var(--panel-header-background)",
			headerStyle: config.headerStyle,

			items: [

				// MESSAGE
				(dialogType == "message" || dialogType == "dialog" || dialogType == "danger") ? {
					type: "html",
					width: "100%",
					padding: "3.2rem",
					html: config.message
				} : null,

				// TEXT FIELD
				(dialogType == "text") ? {
					id: "input-box-field",
					type: "text",
					label: config.message,
					labelPosition: "top",
					width: "100%",
					fieldWidth: "100%",
					value: config.defaultValue || ""
				} : null,

				// TEXTAREA FIELD
				(dialogType == "textarea") ? {
					id: "input-box-field",
					type: "textarea",
					label: config.message,
					labelPosition: "top",
					width: "100%",
					fieldWidth: "100%",
					value: config.defaultValue || "",
					rows: config.rows || 5
				} : null,                

				// SELECT FIELD
				(dialogType == "select") ? {
					id: "input-box-field",
					type: "select",
					label: config.message,
					labelPosition: "top",
					width: "100%",
					value: config.defaultValue || "",
					multiple: !!config.multiple,
					options: config.options || [],
					allowValuesNotInList: (config.allowValuesNotInList == true) ? true : false,
					allowClickToDelete: true
				} : null,

				// DIRECTORY FIELD
				(dialogType == "directory") ? {
					id: "input-box-field",
					type: "directory",
					label: config.message,
					labelPosition: "top",
					width: "100%",
					value: config.defaultValue || "",
					multiple: !!config.multiple,
					users: (config.users === false) ? false : true,
					groups: (config.groups === false) ? false : true,
					roles: !!config.roles,
					allowValuesNotInList: false,
					allowClickToDelete: true
				} : null,

				// BUTTONS
				{
					layout: "horizontal",
					margin: "1rem 0 0 0",

					defaultConfig: {
						margin: "0.5rem"
					},
					items: (config.buttonOKPosition == "left" || config.type == "danger") ? [buttonOK, buttonCancel] : [buttonCancel, buttonOK]
				}
			],

			// Manage the keyboard shortcuts
			// - Enter to validate
			// - Esc to cancel
			events: {
				onkeydown(event) {
					if (event.key == "Escape") {
						this.close()
					} else if (event.key == "Enter") {
						this.validate()
					}
				}
			},

			methods: {
				// Focus the panel or the input field so that keyboard events can be listened to
				// _afterRender() {
				//     if (dialogType == "text" || dialogType == "textarea" || dialogType == "select" || dialogType == "directory") {
				//         setTimeout(() => $("input-box-field").focus(), 100)
				//     } else {
				//         setTimeout(() => this.panelBody.focus(), 100)
				//     }
				// },

				// Clicked on the OK button
				async validate() {
					if (config.action) {
						// If there is an action to handle the entered value
						const newValue = ((dialogType == "text") || dialogType == "textarea" || (dialogType == "select") || (dialogType == "directory")) ? $("input-box-field").getValue() : true
						const result = await config.action(newValue)
						if ((config.autoClose == null) || (config.autoClose == true) || (result == true)) this.close()
					} else {
						// Otherwise, just close
						this.close()
					}
				}
			}
		}

		// Optionally methods
		if (config.methods) Object.assign(dialogConfig.methods, config.methods)

		// Optionally bind events
		if (config.events) Object.assign(dialogConfig.events, config.events)

		// Generate the dialog using a Panel
		let dialog = createPanel(dialogConfig).render()

		return dialog
	}
}

/**
 * Shorthand to create a new Dialog window. See [kiss.ui.Dialog](kiss.ui.Dialog.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createDialog = (config) => new kiss.ui.Dialog(config)