UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

349 lines (348 loc) 11.9 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Released under MIT see LICENSE.txt in the project root for license information. * Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Component } from "../../component/component.js"; import { autobind, throttle } from "../../decorators/index.js"; import { Dom } from "../../dom/dom.js"; import { eventEmitter, getContainer } from "../../global.js"; import { attr, css, isString, kebabCase, markOwner, position, ucfirst } from "../../helpers/index.js"; import { assert } from "../../helpers/utils/assert.js"; import { UIElement } from "../element.js"; import { UIGroup } from "../group/group.js"; const EVENTS_FOR_AUTOCLOSE = [ 'escape', 'cut', 'delete', 'backSpaceAfterDelete', 'beforeCommandDelete' ]; export class Popup extends UIGroup { className() { return 'Popup'; } appendChildToContainer(childContainer) { const content = this.getElm('content'); assert(content, 'Content element should exist'); content.appendChild(childContainer); } updateParentElement(target) { if (target !== this && Component.isInstanceOf(target, Popup)) { this.__childrenPopups.forEach(popup => { if (!target.closest(popup) && popup.isOpened) { popup.close(); } }); if (!this.__childrenPopups.has(target)) { this.j.e.on(target, 'beforeClose', () => { this.__childrenPopups.delete(target); }); } this.__childrenPopups.add(target); } return super.updateParentElement(target); } /** * Set popup content */ setContent(content) { if (this.allChildren.length) { throw new Error('Remove children'); } if (Component.isInstanceOf(content, UIElement)) { this.append(content); } else { const elm = isString(content) ? this.j.c.fromHTML(content) : content; this.appendChildToContainer(elm); } this.updatePosition(); return this; } /** * Open popup near with some bound */ open(getBound, keepPosition = false, parentContainer) { markOwner(this.jodit, this.container); this.container.classList.add(`jodit_theme_${this.jodit.o.theme}`); this.__calculateZIndex(); this.isOpened = true; this.__addGlobalListeners(); this.__targetBound = !keepPosition ? getBound : this.getKeepBound(getBound); if (parentContainer) { parentContainer.appendChild(this.container); } else { const popupContainer = getContainer(this.jodit, Popup); if (parentContainer !== this.container.parentElement) { popupContainer.appendChild(this.container); } } this.updatePosition(); this.j.e.fire(this, 'afterOpen'); this.j.e.fire('afterOpenPopup', this); return this; } __calculateZIndex() { if (this.container.style.zIndex) { return; } const checkView = (view) => { const zIndex = view.container.style.zIndex || view.o.zIndex; if (zIndex) { this.setZIndex(1 + parseInt(zIndex.toString(), 10)); return true; } return false; }; const { j } = this; if (checkView(j)) { return; } let pe = this.parentElement; while (pe) { if (checkView(pe.j)) { return; } if (pe.container.style.zIndex) { this.setZIndex(1 + parseInt(pe.container.style.zIndex.toString(), 10)); return; } if (!pe.parentElement && pe.container.parentElement) { const elm = UIElement.closestElement(pe.container.parentElement, UIElement); if (elm) { pe = elm; continue; } } pe = pe.parentElement; } } /** * Calculate static bound for point */ getKeepBound(getBound) { const oldBound = getBound(); const elmUnderCursor = this.od.elementFromPoint(oldBound.left, oldBound.top); if (!elmUnderCursor) { return getBound; } const element = Dom.isHTMLElement(elmUnderCursor) ? elmUnderCursor : elmUnderCursor.parentElement; const oldPos = position(element, this.j); return () => { const bound = getBound(); const newPos = position(element, this.j); return { ...bound, top: bound.top + (newPos.top - oldPos.top), left: bound.left + (newPos.left - oldPos.left) }; }; } /** * Update container position */ updatePosition() { if (!this.isOpened) { return this; } const [pos, strategy] = this.__calculatePosition(this.__targetBound(), this.viewBound(), position(this.container, this.j)); this.setMod('strategy', strategy); css(this.container, { left: pos.left, top: pos.top }); this.__childrenPopups.forEach(popup => popup.updatePosition()); return this; } __throttleUpdatePosition() { this.updatePosition(); } /** * Calculate start point */ __calculatePosition(target, view, container, defaultStrategy = this.strategy) { const x = { left: target.left, right: target.left - (container.width - target.width) }, y = { bottom: target.top + target.height, top: target.top - container.height }; const list = Object.keys(x).reduce((keys, xKey) => keys.concat(Object.keys(y).map(yKey => `${xKey}${ucfirst(yKey)}`)), []); const getPointByStrategy = (strategy) => { const [xKey, yKey] = kebabCase(strategy).split('-'); return { left: x[xKey], top: y[yKey], width: container.width, height: container.height }; }; const getMatchStrategy = (inBox) => { let strategy = null; if (Popup.boxInView(getPointByStrategy(defaultStrategy), inBox)) { strategy = defaultStrategy; } else { strategy = list.find((key) => { if (Popup.boxInView(getPointByStrategy(key), inBox)) { return key; } return; }) || null; } return strategy; }; // Try to find match position inside Jodit.container let strategy = getMatchStrategy(position(this.j.container, this.j)); // If not found or is not inside window view if (!strategy || !Popup.boxInView(getPointByStrategy(strategy), view)) { // Find match strategy inside window view strategy = getMatchStrategy(view) || strategy || defaultStrategy; } return [getPointByStrategy(strategy), strategy]; } /** * Check if one box is inside second */ static boxInView(box, view) { const accuracy = 2; return (box.top - view.top >= -accuracy && box.left - view.left >= -accuracy && view.top + view.height - (box.top + box.height) >= -accuracy && view.left + view.width - (box.left + box.width) >= -accuracy); } /** * Close popup */ close() { if (!this.isOpened) { return this; } this.isOpened = false; this.__childrenPopups.forEach(popup => popup.close()); this.j.e.fire(this, 'beforeClose'); this.j.e.fire('beforePopupClose', this); this.__removeGlobalListeners(); Dom.safeRemove(this.container); return this; } /** * Close popup if click was in outside */ __closeOnOutsideClick(e) { if (!this.isOpened || this.isOwnClick(e)) { return; } this.close(); } isOwnClick(e) { if (!e.target) { return false; } const box = UIElement.closestElement(e.target, Popup); return Boolean(box && (this === box || box.closest(this))); } __addGlobalListeners() { const up = this.__throttleUpdatePosition, ow = this.ow; eventEmitter.on('closeAllPopups', this.close); if (this.smart) { this.j.e .on(EVENTS_FOR_AUTOCLOSE, this.close) .on('mousedown touchstart', this.__closeOnOutsideClick) .on(ow, 'mousedown touchstart', this.__closeOnOutsideClick); } this.j.e .on('closeAllPopups', this.close) .on('resize', up) .on(this.container, 'scroll mousewheel', up) .on(ow, 'scroll', up) .on(ow, 'resize', up); Dom.up(this.j.container, box => { box && this.j.e.on(box, 'scroll mousewheel', up); }); } __removeGlobalListeners() { const up = this.__throttleUpdatePosition, ow = this.ow; eventEmitter.off('closeAllPopups', this.close); if (this.smart) { this.j.e .off(EVENTS_FOR_AUTOCLOSE, this.close) .off('mousedown touchstart', this.__closeOnOutsideClick) .off(ow, 'mousedown touchstart', this.__closeOnOutsideClick); } this.j.e .off('closeAllPopups', this.close) .off('resize', up) .off(this.container, 'scroll mousewheel', up) .off(ow, 'scroll', up) .off(ow, 'resize', up); if (this.j.container.isConnected) { Dom.up(this.j.container, box => { box && this.j.e.off(box, 'scroll mousewheel', up); }); } } /** * Set ZIndex */ setZIndex(index) { this.container.style.zIndex = index.toString(); } constructor(jodit, smart = true) { super(jodit); this.smart = smart; this.isOpened = false; this.strategy = 'leftBottom'; this.viewBound = () => ({ left: 0, top: 0, width: this.ow.innerWidth, height: this.ow.innerHeight }); this.__childrenPopups = new Set(); attr(this.container, 'role', 'popup'); } render() { return `<div> <div class="&__content"></div> </div>`; } /** @override **/ destruct() { this.close(); return super.destruct(); } } __decorate([ autobind ], Popup.prototype, "updatePosition", null); __decorate([ throttle(10), autobind ], Popup.prototype, "__throttleUpdatePosition", null); __decorate([ autobind ], Popup.prototype, "close", null); __decorate([ autobind ], Popup.prototype, "__closeOnOutsideClick", null);