jodit
Version:
Jodit is an awesome and useful wysiwyg editor with filebrowser
740 lines (739 loc) • 23.8 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
*/
import * as consts from "../constants.js";
import { globalDocument, INSEPARABLE_TAGS, LIST_TAGS, NO_EMPTY_TAGS, TEMP_ATTR } from "../constants.js";
import { toArray } from "../helpers/array/to-array.js";
import { isArray, isFunction, isHTML, isMarker, isSet, isString, isVoid } from "../helpers/checker/index.js";
import { trim } from "../helpers/string/trim.js";
import { $$, assert, attr, call, css, dataBind, error } from "../helpers/utils/index.js";
/**
* Module for working with DOM
*/
export class Dom {
constructor() {
throw new Error('Dom is static module');
}
/**
* Remove all content from element
*/
static detach(node) {
while (node && node.firstChild) {
node.removeChild(node.firstChild);
}
}
/**
* Wrap all inline next siblings
*/
static wrapNextInline(current, tag, editor) {
let tmp;
const first = current;
let last = current;
editor.s.save();
let needFindNext = false;
do {
needFindNext = false;
tmp = last.nextSibling;
if (tmp && !Dom.isBlock(tmp) && !Dom.isTag(tmp, 'br')) {
needFindNext = true;
last = tmp;
}
} while (needFindNext);
return Dom.__wrapElements(tag, editor, first, last);
}
static __wrapElements(tag, editor, first, last) {
const wrapper = isString(tag) ? editor.createInside.element(tag) : tag;
if (first.parentNode) {
first.parentNode.insertBefore(wrapper, first);
}
let next = first;
while (next) {
next = first.nextSibling;
wrapper.appendChild(first);
if (first === last || !next) {
break;
}
first = next;
}
editor.s.restore();
return wrapper;
}
/**
* Wrap all inline siblings
*/
static wrapInline(current, tag, editor) {
let tmp;
let first = current;
let last = current;
editor.s.save();
let needFindNext = false;
do {
needFindNext = false;
tmp = first.previousSibling;
if (tmp && !Dom.isBlock(tmp)) {
needFindNext = true;
first = tmp;
}
} while (needFindNext);
do {
needFindNext = false;
tmp = last.nextSibling;
if (tmp && !Dom.isBlock(tmp)) {
needFindNext = true;
last = tmp;
}
} while (needFindNext);
return Dom.__wrapElements(tag, editor, first, last);
}
/**
* Wrap node inside another node
*/
static wrap(current, tag, create) {
const wrapper = isString(tag) ? create.element(tag) : tag;
if (Dom.isNode(current)) {
if (!current.parentNode) {
throw error('Element should be in DOM');
}
current.parentNode.insertBefore(wrapper, current);
wrapper.appendChild(current);
}
else {
const fragment = current.extractContents();
current.insertNode(wrapper);
wrapper.appendChild(fragment);
}
return wrapper;
}
/**
* Remove parent of node and insert this node instead that parent
*/
static unwrap(node) {
const parent = node.parentNode;
if (parent) {
while (node.firstChild) {
parent.insertBefore(node.firstChild, node);
}
Dom.safeRemove(node);
}
}
/**
* Call functions for all nodes between `start` and `end`
*/
static between(start, end, callback) {
let next = start;
while (next && next !== end) {
if (start !== next && callback(next)) {
break;
}
let step = next.firstChild || next.nextSibling;
if (!step) {
while (next && !next.nextSibling) {
next = next.parentNode;
}
step = next === null || next === void 0 ? void 0 : next.nextSibling;
}
next = step;
}
}
static replace(elm, newTagName, create, withAttributes = false, notMoveContent = false) {
if (isHTML(newTagName)) {
assert(create, 'Need create instance for new tag');
newTagName = create.fromHTML(newTagName);
}
let tag;
if (isString(newTagName)) {
assert(create, 'Need create instance for new tag');
tag = create.element(newTagName);
}
else {
tag = newTagName;
}
if (!notMoveContent) {
while (elm.firstChild) {
tag.appendChild(elm.firstChild);
}
}
if (withAttributes && Dom.isElement(elm) && Dom.isElement(tag)) {
toArray(elm.attributes).forEach(attr => {
tag.setAttribute(attr.name, attr.value);
});
}
if (elm.parentNode) {
elm.parentNode.replaceChild(tag, elm);
}
return tag;
}
/**
* Checks whether the Node text and blank (in this case it may contain invisible auxiliary characters ,
* it is also empty )
*
* @param node - The element of wood to be checked
*/
static isEmptyTextNode(node) {
return (Dom.isText(node) &&
(!node.nodeValue ||
node.nodeValue
.replace(consts.INVISIBLE_SPACE_REG_EXP(), '')
.trim().length === 0));
}
static isEmptyContent(node) {
return Dom.each(node, (elm) => Dom.isEmptyTextNode(elm));
}
/**
* The node is editable
*/
static isContentEditable(node, root) {
return (Dom.isNode(node) &&
!Dom.closest(node, elm => Dom.isElement(elm) &&
elm.getAttribute('contenteditable') === 'false', root));
}
static isEmpty(node, condNoEmptyElement = NO_EMPTY_TAGS) {
if (!node) {
return true;
}
let cond;
if (!isFunction(condNoEmptyElement)) {
cond = (elm) => condNoEmptyElement.has(elm.nodeName.toLowerCase());
}
else {
cond = condNoEmptyElement;
}
const emptyText = (node) => node.nodeValue == null || trim(node.nodeValue).length === 0;
if (Dom.isText(node)) {
return emptyText(node);
}
return (!(Dom.isElement(node) && cond(node)) &&
Dom.each(node, (elm) => {
if ((Dom.isText(elm) && !emptyText(elm)) ||
(Dom.isElement(elm) && cond(elm))) {
return false;
}
}));
}
/**
* Returns true if it is a DOM node
*/
static isNode(object) {
// Duck-typing
return Boolean(object &&
isString(object.nodeName) &&
typeof object.nodeType === 'number' &&
object.childNodes &&
isFunction(object.appendChild));
}
/**
* Check if element is table cell
*/
static isCell(elm) {
return (Dom.isNode(elm) && (elm.nodeName === 'TD' || elm.nodeName === 'TH'));
}
/**
* Check if element is a list element UL or OL
*/
static isList(elm) {
return Dom.isTag(elm, LIST_TAGS);
}
/**
* Check if element is a part of list element LI
*/
static isLeaf(elm) {
return Dom.isTag(elm, 'li');
}
/**
* Check is element is Image element
*/
static isImage(elm) {
return (Dom.isNode(elm) && /^(img|svg|picture|canvas)$/i.test(elm.nodeName));
}
/**
* Check the `node` is a block element
* @param node - Object to check
*/
static isBlock(node) {
return (!isVoid(node) &&
typeof node === 'object' &&
Dom.isNode(node) &&
consts.IS_BLOCK.test(node.nodeName));
}
/**
* Check if element is text node
*/
static isText(node) {
return Boolean(node && node.nodeType === Node.TEXT_NODE);
}
/**
* Check if element is comment node
*/
static isComment(node) {
return Boolean(node && node.nodeType === Node.COMMENT_NODE);
}
/**
* Check if element is element node
*/
static isElement(node) {
var _a;
if (!Dom.isNode(node)) {
return false;
}
const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
return Boolean(win && node.nodeType === Node.ELEMENT_NODE);
}
/**
* Check if element is document fragment
*/
static isFragment(node) {
var _a;
if (!Dom.isNode(node)) {
return false;
}
const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
return Boolean(win && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE);
}
/**
* Check if element is HTMLElement node
*/
static isHTMLElement(node) {
var _a;
if (!Dom.isNode(node)) {
return false;
}
const win = (_a = node.ownerDocument) === null || _a === void 0 ? void 0 : _a.defaultView;
return Boolean(win && node instanceof win.HTMLElement);
}
/**
* Check element is inline block
*/
static isInlineBlock(node) {
return (Dom.isElement(node) &&
!/^(BR|HR)$/i.test(node.tagName) &&
['inline', 'inline-block'].indexOf(css(node, 'display').toString()) !== -1);
}
/**
* It's block and it can be split
*/
static canSplitBlock(node) {
return (!isVoid(node) &&
Dom.isHTMLElement(node) &&
Dom.isBlock(node) &&
!/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) &&
node.style !== undefined &&
!/^(fixed|absolute)/i.test(node.style.position));
}
/**
* Get last matched node inside root
*/
static last(root, condition) {
let last = root === null || root === void 0 ? void 0 : root.lastChild;
if (!last) {
return null;
}
do {
if (condition(last)) {
return last;
}
let next = last.lastChild;
if (!next) {
next = last.previousSibling;
}
if (!next && last.parentNode !== root) {
do {
last = last.parentNode;
} while (last &&
!(last === null || last === void 0 ? void 0 : last.previousSibling) &&
last.parentNode !== root);
next = last === null || last === void 0 ? void 0 : last.previousSibling;
}
last = next;
} while (last);
return null;
}
/**
* Find previous node
*/
static prev(node, condition, root, withChild = true) {
return Dom.find(node, condition, root, false, withChild);
}
/**
* Find next node what `condition(next) === true`
*/
static next(node, condition, root, withChild = true) {
return Dom.find(node, condition, root, true, withChild);
}
static prevWithClass(node, className) {
return Dom.prev(node, node => {
return (Dom.isElement(node) && node.classList.contains(className));
}, node.parentNode);
}
static nextWithClass(node, className) {
return Dom.next(node, elm => Dom.isElement(elm) && elm.classList.contains(className), node.parentNode);
}
/**
* Find next/prev node what `condition(next) === true`
*/
static find(node, condition, root, leftToRight = true, withChild = true) {
const gen = this.nextGen(node, root, leftToRight, withChild);
let item = gen.next();
while (!item.done) {
if (condition(item.value)) {
return item.value;
}
item = gen.next();
}
return null;
}
/**
* Find next/prev node what `condition(next) === true`
*/
static *nextGen(start, root, leftToRight = true, withChild = true) {
const stack = [];
let currentNode = start;
do {
let next = leftToRight
? currentNode.nextSibling
: currentNode.previousSibling;
while (next) {
stack.unshift(next);
next = leftToRight ? next.nextSibling : next.previousSibling;
}
yield* this.runInStack(start, stack, leftToRight, withChild);
currentNode = currentNode.parentNode;
} while (currentNode && currentNode !== root);
return null;
}
/**
* It goes through all the internal elements of the node, causing a callback function
*
* @param elm - the element whose children and descendants you want to iterate over
* @param callback - It called for each item found
* @example
* ```javascript
* Jodit.modules.Dom.each(editor.s.current(), function (node) {
* if (node.nodeType === Node.TEXT_NODE) {
* node.nodeValue = node.nodeValue.replace(Jodit.INVISIBLE_SPACE_REG_EX, '') // remove all of the text element codes invisible character
* }
* });
* ```
*/
static each(elm, callback, leftToRight = true) {
const gen = this.eachGen(elm, leftToRight);
let item = gen.next();
while (!item.done) {
if (callback(item.value) === false) {
return false;
}
item = gen.next();
}
return true;
}
static eachGen(root, leftToRight = true) {
return this.runInStack(root, [root], leftToRight);
}
static *runInStack(start, stack, leftToRight, withChild = true) {
while (stack.length) {
const item = stack.pop();
if (withChild) {
let child = leftToRight ? item.lastChild : item.firstChild;
while (child) {
stack.push(child);
child = leftToRight
? child.previousSibling
: child.nextSibling;
}
}
if (start !== item) {
yield item;
}
}
}
/**
* Find next/prev node what `condition(next) === true`
*/
static findWithCurrent(node, condition, root, sibling = 'nextSibling', child = 'firstChild') {
let next = node;
do {
if (condition(next)) {
return next || null;
}
if (child && next && next[child]) {
const nextOne = Dom.findWithCurrent(next[child], condition, next, sibling, child);
if (nextOne) {
return nextOne;
}
}
while (next && !next[sibling] && next !== root) {
next = next.parentNode;
}
if (next && next[sibling] && next !== root) {
next = next[sibling];
}
} while (next && next !== root);
return null;
}
/**
* Get not empty sibling
*/
static findSibling(node, left = true, cond = (n) => !Dom.isEmptyTextNode(n)) {
let sibling = Dom.sibling(node, left);
while (sibling && !cond(sibling)) {
sibling = Dom.sibling(sibling, left);
}
return sibling && cond(sibling) ? sibling : null;
}
/**
* Returns the nearest non-empty sibling
*/
static findNotEmptySibling(node, left) {
return Dom.findSibling(node, left, n => {
var _a;
return (!Dom.isEmptyTextNode(n) &&
Boolean(!Dom.isText(n) || (((_a = n.nodeValue) === null || _a === void 0 ? void 0 : _a.length) && trim(n.nodeValue))));
});
}
/**
* Returns the nearest non-empty neighbor
*/
static findNotEmptyNeighbor(node, left, root) {
return call(left ? Dom.prev : Dom.next, node, n => Boolean(n &&
(!(Dom.isText(n) || Dom.isComment(n)) ||
trim((n === null || n === void 0 ? void 0 : n.nodeValue) || '').length)), root);
}
static sibling(node, left) {
return left ? node.previousSibling : node.nextSibling;
}
/**
* It goes through all the elements in ascending order, and checks to see if they meet the predetermined condition
*/
static up(node, condition, root, checkRoot = false) {
let start = node;
if (!start) {
return null;
}
do {
if (condition(start)) {
return start;
}
if (start === root || !start.parentNode) {
break;
}
start = start.parentNode;
} while (start && start !== root);
if (start === root && checkRoot && condition(start)) {
return start;
}
return null;
}
static closest(node, tagsOrCondition, root) {
let condition;
const lc = (s) => s.toLowerCase();
if (isFunction(tagsOrCondition)) {
condition = tagsOrCondition;
}
else if (isArray(tagsOrCondition) || isSet(tagsOrCondition)) {
const set = isSet(tagsOrCondition)
? tagsOrCondition
: new Set(tagsOrCondition.map(lc));
condition = (tag) => Boolean(tag && set.has(lc(tag.nodeName)));
}
else {
condition = (tag) => Boolean(tag && lc(tagsOrCondition) === lc(tag.nodeName));
}
return Dom.up(node, condition, root);
}
/**
* Furthest parent node matching condition
*/
static furthest(node, condition, root) {
let matchedParent = null, current = node === null || node === void 0 ? void 0 : node.parentElement;
while (current && current !== root) {
if (condition(current)) {
matchedParent = current;
}
current = current === null || current === void 0 ? void 0 : current.parentElement;
}
return matchedParent;
}
/**
* Append new element in the start of root
*/
static appendChildFirst(root, newElement) {
const child = root.firstChild;
if (child) {
if (child !== newElement) {
root.insertBefore(newElement, child);
}
}
else {
root.appendChild(newElement);
}
}
/**
* Insert newElement after element
*/
static after(elm, newElement) {
const { parentNode } = elm;
if (!parentNode) {
return;
}
if (parentNode.lastChild === elm) {
parentNode.appendChild(newElement);
}
else {
parentNode.insertBefore(newElement, elm.nextSibling);
}
}
/**
* Insert newElement before element
*/
static before(elm, newElement) {
const { parentNode } = elm;
if (!parentNode) {
return;
}
parentNode.insertBefore(newElement, elm);
}
/**
* Insert newElement as first child inside element
*/
static prepend(root, newElement) {
root.insertBefore(newElement, root.firstChild);
}
static append(root, newElement) {
if (isArray(newElement)) {
newElement.forEach(node => {
this.append(root, node);
});
}
else {
root.appendChild(newElement);
}
}
/**
* Move all content to another element
*/
static moveContent(from, to, inStart = false, filter = () => true) {
const fragment = (from.ownerDocument || globalDocument).createDocumentFragment();
toArray(from.childNodes)
.filter(elm => {
if (filter(elm)) {
return true;
}
Dom.safeRemove(elm);
return false;
})
.forEach((node) => {
fragment.appendChild(node);
});
if (!inStart || !to.firstChild) {
to.appendChild(fragment);
}
else {
to.insertBefore(fragment, to.firstChild);
}
}
/**
* Check root contains child or equal child
*/
static isOrContains(root, child, onlyContains = false) {
if (root === child) {
return !onlyContains;
}
return Boolean(child && root && this.up(child, nd => nd === root, root, true));
}
/**
* Safe remove element from DOM
*/
static safeRemove(...nodes) {
nodes.forEach(node => Dom.isNode(node) &&
node.parentNode &&
node.parentNode.removeChild(node));
}
static safeInsertNode(range, node) {
range.collapsed || range.deleteContents();
const child = Dom.isFragment(node) ? node.lastChild : node;
if (range.startContainer === range.endContainer &&
range.collapsed &&
Dom.isTag(range.startContainer, INSEPARABLE_TAGS)) {
Dom.after(range.startContainer, node);
}
else {
range.insertNode(node);
child && range.setStartBefore(child);
}
range.collapse(true);
// https://developer.mozilla.org/en-US/docs/Web/API/Range/insertNode
// if the new node is to be added to a text Node, that Node is split at the
// insertion point, and the insertion occurs between the two text nodes.
[node.nextSibling, node.previousSibling].forEach(n => Dom.isText(n) && !n.nodeValue && Dom.safeRemove(n));
}
/**
* Hide element
*/
static hide(node) {
if (!node) {
return;
}
dataBind(node, '__old_display', node.style.display);
node.style.display = 'none';
}
/**
* Show element
*/
static show(node) {
if (!node) {
return;
}
const display = dataBind(node, '__old_display');
if (node.style.display === 'none') {
node.style.display = display || '';
}
}
static isTag(node, tagNames) {
if (!this.isElement(node)) {
return false;
}
const nameL = node.tagName.toLowerCase();
const nameU = node.tagName.toUpperCase();
if (tagNames instanceof Set) {
return tagNames.has(nameL) || tagNames.has(nameU);
}
if (Array.isArray(tagNames)) {
throw new TypeError('Dom.isTag does not support array');
}
const tags = tagNames;
if (nameL === tags || nameU === tags) {
return true;
}
return false;
}
/**
* Marks an item as temporary
*/
static markTemporary(element, attributes) {
attributes && attr(element, attributes);
attr(element, TEMP_ATTR, true);
return element;
}
/**
* Check if element is temporary
*/
static isTemporary(element) {
if (!Dom.isElement(element)) {
return false;
}
return isMarker(element) || attr(element, TEMP_ATTR) === 'true';
}
/**
* Replace temporary elements from string
*/
static replaceTemporaryFromString(value) {
return value.replace(/<([a-z]+)[^>]+data-jodit-temp[^>]+>(.+?)<\/\1>/gi, '$2');
}
/**
* Get temporary list
*/
static temporaryList(root) {
return $$(`[${TEMP_ATTR}]`, root);
}
}