jodit
Version:
Jodit is an awesome and useful wysiwyg editor with filebrowser
349 lines (348 loc) • 11.9 kB
JavaScript
/*!
* 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);