UNPKG

suneditor

Version:

Vanilla JavaScript based WYSIWYG web editor

364 lines (319 loc) 12.3 kB
import { dom, env, keyCodeMap } from '../../helper'; const { _w } = env; const DIRECTION_CURSOR_MAP = { w: 'ns-resize', h: 'ew-resize', c: 'nwse-resize', wRTL: 'ns-resize', hRTL: 'ew-resize', cRTL: 'nesw-resize' }; /** * @class * @description Modal window module */ class Modal { #$; /** @type {HTMLElement} */ #modalArea; /** @type {HTMLElement} */ #modalInner; /** @type {HTMLElement} */ #resizeBody; #closeListener; #globalEventHandlers; #bindClose = null; #hasNoCloseButton = false; #bindCloseClick = null; #currentHandle = null; #resizeDir = ''; #offetTop = 0; #offetLeft = 0; #bindClose_mousemove = null; #bindClose_mouseup = null; /** * @description Modal window module * @param {*} inst The instance object that called the constructor. * @param {SunEditor.Deps} $ Kernel dependencies * @param {Element} element Modal element */ constructor(inst, $, element) { this.#$ = $; // members this.inst = inst; this.kind = inst.constructor.key || inst.constructor.name; this.form = /** @type {HTMLElement} */ (element); this.isUpdate = false; /** @type {HTMLInputElement} */ this.focusElement = element.querySelector('[data-focus]'); this.#modalArea = this.#$.contextProvider.carrierWrapper.querySelector('.se-modal'); this.#modalInner = this.#$.contextProvider.carrierWrapper.querySelector('.se-modal .se-modal-inner'); this.#closeListener = [this.#CloseListener.bind(this), this.#OnClick_dialog.bind(this)]; // add element this.#modalInner.appendChild(element); // init this.#$.eventManager.addEvent(element.querySelector('form'), 'submit', this.#Action.bind(this)); this.#hasNoCloseButton = !this.#$.eventManager.addEvent(element.querySelector('[data-command="close"]'), 'click', this.close.bind(this)); // resize if (element.querySelector('.se-modal-resize-handle-w') || element.querySelector('.se-modal-resize-handle-h') || element.querySelector('.se-modal-resize-handle-c') || element.querySelector('.se-modal-resize-form')) { if (!(this.#resizeBody = element.querySelector('.se-modal-resize-form')) && (this.#resizeBody = element.querySelector('.se-modal-body'))) { this.#$.eventManager.addEvent(element.querySelector('.se-modal-resize-handle-w'), 'mousedown', this.#OnResizeMouseDown.bind(this, 'w')); this.#$.eventManager.addEvent(element.querySelector('.se-modal-resize-handle-h'), 'mousedown', this.#OnResizeMouseDown.bind(this, 'h')); this.#$.eventManager.addEvent(element.querySelector('.se-modal-resize-handle-c'), 'mousedown', this.#OnResizeMouseDown.bind(this, 'c')); this.#globalEventHandlers = { mousemove: this.#OnResize.bind(this), mouseup: this.#OnResizeMouseUp.bind(this), }; } } } /** * @description Create a file input tag in the modal window. * @param {{icons: SunEditor.Deps['icons'], lang: SunEditor.Deps['lang']}} param0 - icons and language object * @param {{acceptedFormats?: string, allowMultiple?: boolean}} param1 - options * - acceptedFormats: `"image/*, video/*, audio/*"`, etc. * - allowMultiple: `true` or `false` * @returns {string} HTML string * @example * // Inside a plugin's modal HTML template: * const html = Modal.CreateFileInput( * { icons, lang }, * { acceptedFormats: 'image/*', allowMultiple: true } * ); */ static CreateFileInput({ icons, lang }, { acceptedFormats, allowMultiple }) { return /*html*/ ` <div class="se-modal-form-files"> <div class="se-flex-input-wrapper"> <div class="se-input-form-abs"> <div> <div class="se-input-file-w"> <div class="se-input-file-icon-up">${icons.upload_tray}</div> <div class="se-input-file-icon-files">${icons.file_plus}</div> <span class="se-input-file-cnt"></span> </div> </div> </div> <input class="se-input-form __se__file_input" data-focus type="file" accept="${acceptedFormats}"${allowMultiple ? ' multiple="multiple"' : ''}/> </div> <button type="button" class="se-btn se-modal-files-edge-button se-file-remove se-tooltip" aria-label="${lang.remove}"> ${icons.selection_remove} ${dom.utils.createTooltipInner(lang.remove)} </button> </div>`; } /** * @description A function called when the contents of `input` have changed and you want to adjust the style. * @param {Element} wrapper - Modal file input wrapper(`.se-flex-input-wrapper`) * @param {FileList|File[]} files - FileList object */ static OnChangeFile(wrapper, files) { if (!wrapper || !files) return; const fileCnt = /** @type {HTMLElement} */ (wrapper.querySelector('.se-input-file-cnt')); const fileUp = /** @type {HTMLElement} */ (wrapper.querySelector('.se-input-file-icon-up')); const fileSelected = /** @type {HTMLElement} */ (wrapper.querySelector('.se-input-file-icon-files')); if (files.length > 1) { fileUp.style.display = 'none'; fileSelected.style.display = 'inline-block'; fileCnt.style.display = ''; fileCnt.textContent = ` ..${files.length}`; } else if (files.length > 0) { fileUp.style.display = 'none'; fileSelected.style.display = 'none'; fileCnt.style.display = 'block'; fileCnt.textContent = files[0].name; } else { fileUp.style.display = 'inline-block'; fileSelected.style.display = 'none'; fileCnt.style.display = ''; fileCnt.textContent = ''; } } /** * @description Open a modal plugin * - The plugin's `init` method is called. */ open() { this.#$.ui.offCurrentModal(); this.#fixCurrentController(true); if (this.#hasNoCloseButton) { if (this.#bindCloseClick) this.#$.eventManager.removeEvent(this.#bindCloseClick); this.#bindCloseClick = this.#$.eventManager.addEvent(this.#modalInner, 'click', this.#closeListener[1]); } this.#bindClose &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose); this.#bindClose = this.#$.eventManager.addGlobalEvent('keydown', this.#closeListener[0]); this.isUpdate = this.kind === this.#$.ui.currentControllerName; this.#$.ui.opendModal = this; if (!this.isUpdate) this.inst.modalInit?.(); this.inst.modalOn?.(this.isUpdate); dom.utils.addClass(this.#modalArea, 'se-backdrop-show'); dom.utils.addClass(this.form, 'se-modal-show'); if (this.#resizeBody) { const offset = this.#saveOffset(); const { maxWidth, maxHeight } = _w.getComputedStyle(this.form); const mw = `${this.form.offsetWidth - offset.width}px`; const mh = `${this.form.offsetTop + (this.form.offsetHeight - this.#resizeBody.offsetHeight)}px`; // set max if (maxWidth && typeof this.#resizeDir === 'string') dom.utils.setStyle(this.#resizeBody, 'max-width', `calc(${maxWidth} - ${mw})`); if (maxHeight) dom.utils.setStyle(this.#resizeBody, 'max-height', `calc(${maxHeight} - ${mh})`); } if (this.focusElement) this.focusElement.focus(); } /** * @description Close a modal plugin * - The plugin's `init` and `modalOff` method is called. */ close() { this.#removeGlobalEvent(); this.#fixCurrentController(false); _w.setTimeout(() => { this.#$.ui.opendModal = null; }, 0); if (this.#hasNoCloseButton) { if (this.#bindCloseClick) { this.#$.eventManager.removeEvent(this.#bindCloseClick); this.#bindCloseClick = null; } } this.#bindClose &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose); // close dom.utils.removeClass(this.#modalArea, 'se-backdrop-show'); dom.utils.removeClass(this.form, 'se-modal-show'); this.inst.modalInit?.(); this.inst.modalOff?.(this.isUpdate); if (!this.isUpdate) this.#$.focusManager.focus(); } /** * @description Fixes the current controller's display state when the modal is opened or closed. * @param {boolean} fixed - Whether to fix or unfix the controller. */ #fixCurrentController(fixed) { const cont = this.#$.ui.opendControllers; for (let i = 0; i < cont.length; i++) { cont[i].fixed = fixed; cont[i].form.style.display = fixed ? 'none' : 'block'; } } /** * @description Saves the current offset position of the modal for resizing calculations. * @returns {import('../../core/logic/dom/offset').OffsetGlobalInfo} The offset position of the modal. */ #saveOffset() { const offset = this.#$.offset.getGlobal(this.#resizeBody); this.#offetTop = offset.top; this.#offetLeft = offset.left; return offset; } /** * @description Adds global event listeners for resizing the modal. * @param {string} dir - The direction in which resizing is occurring. */ #addGlobalEvent(dir) { this.#removeGlobalEvent(); this.#$.ui.enableBackWrapper(DIRECTION_CURSOR_MAP[dir]); this.#bindClose_mousemove = this.#$.eventManager.addGlobalEvent('mousemove', this.#globalEventHandlers.mousemove, true); this.#bindClose_mouseup = this.#$.eventManager.addGlobalEvent('mouseup', this.#globalEventHandlers.mouseup, true); } /** * @description Removes global event listeners related to modal resizing. */ #removeGlobalEvent() { this.#$.ui.disableBackWrapper(); this.#bindClose_mousemove &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose_mousemove); this.#bindClose_mouseup &&= this.#$.eventManager.removeGlobalEvent(this.#bindClose_mouseup); } /** * The loading bar is executed before `modalAction` is executed. * return type - * `true` : the loading bar and modal window are closed. * `false` : only the loading bar is closed. * `undefined` : only the modal window is closed. * - * exception occurs : the modal window and loading bar are closed. * @param {SubmitEvent} e - Event object */ async #Action(e) { e.preventDefault(); e.stopPropagation(); this.#$.ui.showLoading(); try { const result = await this.inst.modalAction(); if (result === false) { this.#$.ui.hideLoading(); } else if (result === undefined) { this.close(); } else { this.close(); this.#$.ui.hideLoading(); } } catch (error) { this.close(); this.#$.ui.hideLoading(); throw Error(`[SUNEDITOR.Modal[${this.kind}].warn] ${error.message}`); } } /** * @param {MouseEvent} e - Event object */ #OnClick_dialog(e) { const eventTarget = dom.query.getEventTarget(e); if (/close/.test(eventTarget.getAttribute('data-command')) || eventTarget === this.#modalInner) { this.close(); } } /** * @param {KeyboardEvent} e - Event object */ #CloseListener(e) { if (!keyCodeMap.isEsc(e.code)) return; this.close(); } /** ---------- Resize events ---------- */ /** * @param {string} dir - The direction in which the resize handle is located. * @param {MouseEvent} e - Event object */ #OnResizeMouseDown(dir, e) { this.#currentHandle = dom.query.getEventTarget(e); dom.utils.addClass(this.#currentHandle, 'active'); this.#addGlobalEvent((this.#resizeDir = dir + (this.#$.options.get('_rtl') ? 'RTL' : ''))); } /** * @param {MouseEvent} e - Event object */ #OnResize(e) { switch (this.#resizeDir) { case 'w': case 'wRTL': { const h = e.clientY - this.#offetTop - this.#resizeBody.offsetHeight; this.#resizeBody.style.height = this.#resizeBody.offsetHeight + h + 'px'; break; } case 'h': { const w = e.clientX - this.#offetLeft - this.#resizeBody.offsetWidth; this.#resizeBody.style.width = this.#resizeBody.offsetWidth + w + 'px'; break; } case 'hRTL': { const w = this.#offetLeft - e.clientX; this.#resizeBody.style.width = this.#resizeBody.offsetWidth + w + 'px'; break; } case 'c': { const w = e.clientX - this.#offetLeft - this.#resizeBody.offsetWidth; const h = e.clientY - this.#offetTop - this.#resizeBody.offsetHeight; this.#resizeBody.style.width = this.#resizeBody.offsetWidth + w + 'px'; this.#resizeBody.style.height = this.#resizeBody.offsetHeight + h + 'px'; break; } case 'cRTL': { const w = this.#offetLeft - e.clientX; const h = e.clientY - this.#offetTop - this.#resizeBody.offsetHeight; this.#resizeBody.style.width = this.#resizeBody.offsetWidth + w + 'px'; this.#resizeBody.style.height = this.#resizeBody.offsetHeight + h + 'px'; break; } } this.#saveOffset(); this.inst.modalResize?.(); } #OnResizeMouseUp() { dom.utils.removeClass(this.#currentHandle, 'active'); this.#currentHandle = null; this.#removeGlobalEvent(); } } export default Modal;