/************************************************************************ * This file is part of EspoCRM. * * EspoCRM – Open Source CRM application. * Copyright (C) 2014-2025 EspoCRM, Inc. * Website: https://www.espocrm.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ /** @module ui */ import {marked} from 'marked'; import DOMPurify from 'dompurify'; import $ from 'jquery'; /** * Dialog parameters. * * @typedef {Object} module:ui.Dialog~Params * * @property {string} [className='dialog'] A class-name or multiple space separated. * @property {'static'|true|false} [backdrop='static'] A backdrop. * @property {boolean} [closeButton=true] A close button. * @property {boolean} [collapseButton=false] A collapse button. * @property {string|null} [header] A header HTML. * @property {string} [body] A body HTML. * @property {number|null} [width] A width. * @property {boolean} [removeOnClose=true] To remove on close. * @property {boolean} [draggable=false] Is draggable. * @property {function(): void} [onRemove] An on-remove callback. * @property {function(): void} [onClose] An on-close callback. * @property {function(): void} [onBackdropClick] An on-backdrop-click callback. * @property {string} [container='body'] A container selector. * @property {boolean} [keyboard=true] Enable a keyboard control. The `Esc` key closes a dialog. * @property {boolean} [footerAtTheTop=false] To display a footer at the top. * @property {module:ui.Dialog~Button[]} [buttonList] Buttons. * @property {Array} [dropdownItemList] Dropdown action items. * @property {boolean} [fullHeight] Deprecated. * @property {Number} [bodyDiffHeight] * @property {Number} [screenWidthXs] * @property {boolean} [maximizeButton] Is maximizable. * @property {function()} [onMaximize] On maximize handler. * @property {function()} [onMinimize] On minimize handler. * @property {string} [backdropClassName] A backdrop class name. As of v9.1.0. */ /** * A button or dropdown action item. * * @typedef {Object} module:ui.Dialog~Button * * @property {string} name A name. * @property {boolean} [pullLeft=false] Deprecated. Use the `position` property. * @property {'left'|'right'} [position='left'] A position. * @property {string} [html] HTML. * @property {string} [text] A text. * @property {boolean} [disabled=false] Disabled. * @property {boolean} [hidden=false] Hidden. * @property {'default'|'danger'|'success'|'warning'} [style='default'] A style. * @property {function(Espo.Ui.Dialog, JQueryEventObject): void} [onClick] An on-click callback. * @property {string} [className] An additional class name. * @property {string} [title] A title. */ /** * @type {Espo.Ui.Dialog[]} */ const shownDialogList = []; /** * @alias Espo.Ui.Dialog */ class Dialog { height fitHeight onRemove onClose onBackdropClick buttons screenWidthXs backdropClassName /** * @private * @type {boolean} */ maximizeButton = false /** * @private * @type {HTMLAnchorElement} */ maximizeButtonElement /** * @private * @type {HTMLAnchorElement} */ minimizeButtonElement /** * @private * @type {function()} */ onMaximize /** * @private * @type {function()} */ onMinimize /** * @param {module:ui.Dialog~Params} options Options. */ constructor(options) { options = options || {}; /** @private */ this.className = 'dialog-confirm'; /** @private */ this.backdrop = 'static'; /** @private */ this.closeButton = true; /** @private */ this.collapseButton = false; /** @private */ this.header = null; /** @private */ this.body = ''; /** @private */ this.width = null; /** * @private * @type {module:ui.Dialog~Button[]} */ this.buttonList = []; /** * @private * @type {Array} */ this.dropdownItemList = []; /** @private */ this.removeOnClose = true; /** @private */ this.draggable = false; /** @private */ this.container = 'body'; /** @private */ this.options = options; /** @private */ this.keyboard = true; this.activeElement = document.activeElement; const params = [ 'className', 'backdrop', 'keyboard', 'closeButton', 'collapseButton', 'header', 'body', 'width', 'height', 'fitHeight', 'buttons', 'buttonList', 'dropdownItemList', 'removeOnClose', 'draggable', 'container', 'onRemove', 'onClose', 'onBackdropClick', 'maximizeButton', 'backdropClassName', ]; params.forEach(param => { if (param in options) { this[param] = options[param]; } }); if (options.onMaximize) { this.onMaximize = options.onMaximize; } if (options.onMinimize) { this.onMinimize = options.onMinimize; } /** @private */ this.onCloseIsCalled = false; if (this.buttons && this.buttons.length) { /** * @private * @type {module:ui.Dialog~Button[]} */ this.buttonList = this.buttons; } this.id = 'dialog-' + Math.floor((Math.random() * 100000)); if (typeof this.backdrop === 'undefined') { /** @private */ this.backdrop = 'static'; } const $header = this.getHeader(); const $footer = this.getFooter(); const $body = $('
') .addClass('modal-body body') .html(this.body); const $content = $('
').addClass('modal-content'); if ($header) { $content.append($header); } if ($footer && this.options.footerAtTheTop) { $content.append($footer); } $content.append($body); if ($footer && !this.options.footerAtTheTop) { $content.append($footer); } const $dialog = $('
') .addClass('modal-dialog') .append($content); const $container = $(this.container); $('
') .attr('id', this.id) .attr('class', this.className + ' modal') .attr('role', 'dialog') .attr('tabindex', '-1') .append($dialog) .appendTo($container); /** * An element. * * @type {JQuery} */ this.$el = $('#' + this.id); /** * @private * @type {Element} */ this.el = this.$el.get(0); this.$el.find('header a.close').on('click', () => { //this.close(); }); this.initButtonEvents(); if (this.draggable) { this.$el.find('header').css('cursor', 'pointer'); // noinspection JSUnresolvedReference this.$el.draggable({ handle: 'header', }); } const modalContentEl = this.$el.find('.modal-content'); if (this.width) { modalContentEl.css('width', this.width); modalContentEl.css('margin-left', '-' + (parseInt(this.width.replace('px', '')) / 5) + 'px'); } if (this.removeOnClose) { this.$el.on('hidden.bs.modal', e => { if (this.$el.get(0) === e.target) { if (!this.onCloseIsCalled) { this.close(); } if (this.skipRemove) { return; } this.remove(); } }); } const $window = $(window); this.$el.on('shown.bs.modal', () => { $('.modal-backdrop').not('.stacked').addClass('stacked'); const headerHeight = this.$el.find('.modal-header').outerHeight() || 0; const footerHeight = this.$el.find('.modal-footer').outerHeight() || 0; let diffHeight = headerHeight + footerHeight; if (!options.fullHeight) { diffHeight = diffHeight + options.bodyDiffHeight; } if (this.fitHeight || options.fullHeight) { const processResize = () => { const windowHeight = window.innerHeight; const windowWidth = $window.width(); if (!options.fullHeight && windowHeight < 512) { this.$el.find('div.modal-body').css({ maxHeight: 'none', overflow: 'auto', height: 'none', }); return; } const cssParams = { overflow: 'auto', }; if (options.fullHeight) { cssParams.height = (windowHeight - diffHeight) + 'px'; this.$el.css('paddingRight', 0); } else { if (windowWidth <= options.screenWidthXs) { cssParams.maxHeight = 'none'; } else { cssParams.maxHeight = (windowHeight - diffHeight) + 'px'; } } this.$el.find('div.modal-body').css(cssParams); }; $window.off('resize.modal-height'); $window.on('resize.modal-height', processResize); processResize(); } }); const $documentBody = $(document.body); this.$el.on('hidden.bs.modal', () => { if ($('.modal:visible').length > 0) { $documentBody.addClass('modal-open'); } }); } /** * Get a general container element. * * @return {HTMLDivElement} * @since 9.1.0 */ getElement() { return this.el; } /** * Update the header text. * * @param {string} text * @since 9.1.0 */ setHeaderText(text) { const element = this.el.querySelector('.modal-header .modal-title'); if (!element) { return; } element.textContent = text; } /** @private */ callOnClose() { if (this.onClose) { this.onClose() } } /** @private */ callOnBackdropClick() { if (this.onBackdropClick) { this.onBackdropClick() } } /** @private */ callOnRemove() { if (this.onRemove) { this.onRemove() } } /** * Set action items. * * @param {module:ui.Dialog~Button[]} buttonList * @param {Array} dropdownItemList */ setActionItems(buttonList, dropdownItemList) { this.buttonList = buttonList; this.dropdownItemList = dropdownItemList; } /** * Init button events. */ initButtonEvents() { this.buttonList.forEach(o => { if (typeof o.onClick === 'function') { const $button = $('#' + this.id + ' .modal-footer button[data-name="' + o.name + '"]'); $button.on('click', e => o.onClick(this, e)); } }); this.dropdownItemList.forEach(o => { if (o === false) { return; } if (typeof o.onClick === 'function') { const $button = $('#' + this.id + ' .modal-footer a[data-name="' + o.name + '"]'); $button.on('click', e => o.onClick(this, e)); } }); } /** * @private * @return {JQuery|null} */ getHeader() { if (!this.header) { return null; } const $header = $('
') .addClass('modal-header') .addClass(this.options.fixedHeaderHeight ? 'fixed-height' : '') .append( $('

') .addClass('modal-title') .append( $('') .addClass('modal-title-text') .html(this.header) ) ); if (this.collapseButton) { $header.prepend( $('') .addClass('collapse-button') .attr('role', 'button') .attr('tabindex', '-1') .attr('data-action', 'collapseModal') .append( $('') .addClass('fas fa-minus') ) ); } if (this.maximizeButton) { { const a = document.createElement('a'); a.classList.add('maximize-button'); a.role = 'button'; a.tabIndex = -1; a.setAttribute('data-action', 'maximizeModal'); const icon = document.createElement('span'); icon.classList.add('far', 'fa-window-maximize') a.append(icon); $header.prepend(a); this.maximizeButtonElement = a; a.addEventListener('click', () => { this.maximizeButtonElement.classList.add('hidden'); this.minimizeButtonElement.classList.remove('hidden'); this.el.querySelector('.modal-dialog').classList.add('maximized'); if (this.onMaximize) { this.onMaximize(); } this.getElement().focus(); }); } { const a = document.createElement('a'); a.classList.add('minimize-button', 'hidden'); a.role = 'button'; a.tabIndex = -1; a.setAttribute('data-action', 'minimizeModal'); const icon = document.createElement('span'); icon.classList.add('far', 'fa-window-minimize') a.append(icon); $header.prepend(a); this.minimizeButtonElement = a; a.addEventListener('click', () => { this.minimizeButtonElement.classList.add('hidden'); this.maximizeButtonElement.classList.remove('hidden'); this.el.querySelector('.modal-dialog').classList.remove('maximized'); if (this.onMinimize) { this.onMinimize(); } this.getElement().focus(); }); } } if (this.closeButton) { $header.prepend( $('') .addClass('close') .attr('data-dismiss', 'modal') .attr('role', 'button') .attr('tabindex', '-1') .append( $('') .attr('aria-hidden', 'true') .html('×') ) ); } return $header; } /** * Get a footer. * * @return {JQuery|null} */ getFooter() { if (!this.buttonList.length && !this.dropdownItemList.length) { return null; } const $footer = $('