/************************************************************************ * This file is part of EspoCRM. * * EspoCRM - Open Source CRM application. * Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko * Website: https://www.espocrm.com * * EspoCRM is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * EspoCRM 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with EspoCRM. If not, see http://www.gnu.org/licenses/. * * 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 General Public License version 3. * * In accordance with Section 7(b) of the GNU 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 {module:ui.Dialog~Button[]} [dropdownItemList] Dropdown action items. * @property {boolean} [fullHeight] Deprecated. * @property {Number} [bodyDiffHeight] * @property {Number} [screenWidthXs] */ /** * 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. */ /** * @alias Espo.Ui.Dialog */ class Dialog { height fitHeight onRemove onClose onBackdropClick buttons screenWidthXs /** * @param {module:ui.Dialog~Params} options Options. */ constructor(options) { options = options || {}; /** @private */ this.className = 'dialog'; /** @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 {module:ui.Dialog~Button[]} */ 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; let params = [ 'className', 'backdrop', 'keyboard', 'closeButton', 'collapseButton', 'header', 'body', 'width', 'height', 'fitHeight', 'buttons', 'buttonList', 'dropdownItemList', 'removeOnClose', 'draggable', 'container', 'onRemove', 'onClose', 'onBackdropClick', ]; params.forEach(param => { if (param in options) { this[param] = options[param]; } }); /** @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'; } let $header = this.getHeader(); let $footer = this.getFooter(); let $body = $('
') .addClass('modal-body body') .html(this.body); let $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); } let $dialog = $('
') .addClass('modal-dialog') .append($content); let $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', }); } let 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(); } }); } let $window = $(window); this.$el.on('shown.bs.modal', () => { $('.modal-backdrop').not('.stacked').addClass('stacked'); let headerHeight = this.$el.find('.modal-header').outerHeight() || 0; let footerHeight = this.$el.find('.modal-footer').outerHeight() || 0; let diffHeight = headerHeight + footerHeight; if (!options.fullHeight) { diffHeight = diffHeight + options.bodyDiffHeight; } if (this.fitHeight || options.fullHeight) { let processResize = () => { let windowHeight = window.innerHeight; let windowWidth = $window.width(); if (!options.fullHeight && windowHeight < 512) { this.$el.find('div.modal-body').css({ maxHeight: 'none', overflow: 'auto', height: 'none', }); return; } let 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(); } }); let $documentBody = $(document.body); this.$el.on('hidden.bs.modal', () => { if ($('.modal:visible').length > 0) { $documentBody.addClass('modal-open'); } }); } /** @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 {module:ui.Dialog~Button[]} dropdownItemList */ setActionItems(buttonList, dropdownItemList) { this.buttonList = buttonList; this.dropdownItemList = dropdownItemList; } /** * Init button events. */ initButtonEvents() { this.buttonList.forEach(o => { if (typeof o.onClick === 'function') { let $button = $('#' + this.id + ' .modal-footer button[data-name="' + o.name + '"]'); $button.on('click', e => o.onClick(this, e)); } }); this.dropdownItemList.forEach(o => { if (typeof o.onClick === 'function') { let $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; } let $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.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; } let $footer = $('