UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

784 lines (699 loc) 16.8 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Licensed under GNU General Public License version 2 or later or a commercial license; * Copyright 2013-2019 Valeriy Chupurnov https://xdsoft.net */ import * as consts from '../constants'; import { HTMLTagNames, IJodit, NodeCondition } from '../types'; import { css } from './helpers/'; import { trim } from './helpers/string'; export class Dom { /** * Remove all connetn form element * * @param {Node} node */ static detach(node: Node) { while (node.firstChild) { node.removeChild(node.firstChild); } } /** * * @param {Node} current * @param {String | Node} tag * @param {Jodit} editor * * @return {HTMLElement} */ static wrapInline = ( current: Node, tag: Node | HTMLTagNames, editor: IJodit ): HTMLElement => { let tmp: null | Node, first: Node = current, last: Node = current; const selInfo = editor.selection.save(); let needFindNext: boolean = false; do { needFindNext = false; tmp = first.previousSibling; if (tmp && !Dom.isBlock(tmp, editor.editorWindow)) { needFindNext = true; first = tmp; } } while (needFindNext); do { needFindNext = false; tmp = last.nextSibling; if (tmp && !Dom.isBlock(tmp, editor.editorWindow)) { needFindNext = true; last = tmp; } } while (needFindNext); const wrapper = typeof tag === 'string' ? editor.create.inside.element(tag) : tag; if (first.parentNode) { first.parentNode.insertBefore(wrapper, first); } let next: Node | null = first; while (next) { next = first.nextSibling; wrapper.appendChild(first); if (first === last || !next) { break; } first = next; } editor.selection.restore(selInfo); return wrapper as HTMLElement; }; /** * * @param {Node} current * @param {String | Node} tag * @param {Jodit} editor * * @return {HTMLElement} */ static wrap = ( current: Node, tag: Node | string, editor: IJodit ): HTMLElement | null => { const selInfo = editor.selection.save(); const wrapper = typeof tag === 'string' ? editor.editorDocument.createElement(tag) : tag; if (!current.parentNode) { return null; } current.parentNode.insertBefore(wrapper, current); wrapper.appendChild(current); editor.selection.restore(selInfo); return wrapper as HTMLElement; }; /** * * @param node */ static unwrap(node: Node) { const parent: Node | null = node.parentNode, el = node; if (parent) { while (el.firstChild) { parent.insertBefore(el.firstChild, el); } Dom.safeRemove(el); } } /** * It goes through all the internal elements of the node , causing a callback function * * @param {HTMLElement} elm elements , the internal node is necessary to sort out * @param {Function} callback It called for each item found * @example * ```javascript * Jodit.modules.Dom.each(parent.selection.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: Node | HTMLElement, callback: (this: Node, node: Node) => void | false ): boolean { let node: Node | null | false = elm.firstChild; if (node) { while (node) { if ( callback.call(node, node) === false || !Dom.each(node, callback) ) { return false; } node = Dom.next(node, nd => !!nd, elm); } } return true; } /** * Replace one tag to another transfer content * * @param {Node} elm The element that needs to be replaced by new * @param {string} newTagName tag name for which will change `elm` * @param {boolean} withAttributes=false If true move tag's attributes * @param {boolean} notMoveContent=false false - Move content from elm to newTagName * @param {Document} [doc=document] * @return {Node} Returns a new tag * @example * ```javascript * Jodit.modules.Dom.replace(parent.editor.getElementsByTagName('span')[0], 'p'); * // Replace the first <span> element to the < p > * ``` */ static replace( elm: HTMLElement, newTagName: string | HTMLElement, withAttributes = false, notMoveContent = false, doc: Document ): HTMLElement { const tag: HTMLElement = typeof newTagName === 'string' ? doc.createElement(newTagName) : newTagName; if (!notMoveContent) { while (elm.firstChild) { tag.appendChild(elm.firstChild); } } if (withAttributes) { Array.from(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} node The element of wood to be checked * @return {Boolean} true element is empty */ static isEmptyTextNode(node: Node): boolean { return ( node && node.nodeType === Node.TEXT_NODE && (!node.nodeValue || node.nodeValue.replace(consts.INVISIBLE_SPACE_REG_EXP, '') .length === 0) ); } /** * Check if element is not empty * * @param {Node} node * @param {RegExp} condNoEmptyElement * @return {boolean} */ static isEmpty( node: Node, condNoEmptyElement: RegExp = /^(img|svg|canvas|input|textarea|form)$/ ): boolean { if (!node) { return true; } if (node.nodeType === Node.TEXT_NODE) { return node.nodeValue === null || trim(node.nodeValue).length === 0; } return ( !node.nodeName.toLowerCase().match(condNoEmptyElement) && Dom.each( node as HTMLElement, (elm: Node | null): false | void => { if ( (elm && elm.nodeType === Node.TEXT_NODE && (elm.nodeValue !== null && trim(elm.nodeValue).length !== 0)) || (elm && elm.nodeType === Node.ELEMENT_NODE && condNoEmptyElement.test(elm.nodeName.toLowerCase())) ) { return false; } } ) ); } /** * Returns true if it is a DOM node */ static isNode(object: unknown, win?: Window): object is Node { if ( typeof win === 'object' && win && (typeof (win as any).Node === 'function' || typeof (win as any).Node === 'object') ) { return object instanceof (win as any).Node; // for Iframe Node !== iframe.contentWindow.Node } return false; } /** * Check if element is table cell * * @param {Node} elm * @param {Window} win * @return {boolean} */ static isCell(elm: unknown, win: Window): elm is HTMLTableCellElement { return Dom.isNode(elm, win) && /^(td|th)$/i.test(elm.nodeName); } /** * Check is element is Image element * * @param {Node} elm * @param {Window} win * @return {boolean} */ static isImage(elm: unknown, win: Window): elm is HTMLImageElement { return ( Dom.isNode(elm, win) && /^(img|svg|picture|canvas)$/i.test(elm.nodeName) ); } /** * Check the `node` is a block element * * @param node * @param win * * @return {boolean} */ static isBlock(node: unknown, win: Window): boolean { return ( node && typeof node === 'object' && Dom.isNode(node, win) && consts.IS_BLOCK.test((<Node>node).nodeName) ); } /** * Check element is inline block * * @param node */ static isInlineBlock(node: unknown): boolean { return ( !!node && (<Node>node).nodeType === Node.ELEMENT_NODE && ['inline', 'inline-block'].indexOf( css(node as HTMLElement, 'display').toString() ) !== -1 ); } /** * It's block and it can be split * */ static canSplitBlock(node: any, win: Window): boolean { return ( node && node instanceof (win as any).HTMLElement && this.isBlock(node, win) && !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && node.style !== void 0 && !/^(fixed|absolute)/i.test(node.style.position) ); } /** * Find previous node * * @param {Node} node * @param {function} condition * @param {Node} root * @param {boolean} [withChild=true] * * @return {boolean|Node|HTMLElement|HTMLTableCellElement} false if not found */ static prev( node: Node, condition: NodeCondition, root: HTMLElement, withChild: boolean = true ): false | Node | HTMLElement | HTMLTableCellElement { return Dom.find( node, condition, root, false, 'previousSibling', withChild ? 'lastChild' : false ); } /** * Find next node what `condition(next) === true` * * @param {Node} node * @param {function} condition * @param {Node} root * @param {boolean} [withChild=true] * @return {boolean|Node|HTMLElement|HTMLTableCellElement} */ static next( node: Node, condition: NodeCondition, root: Node | HTMLElement, withChild: boolean = true ): false | Node | HTMLElement | HTMLTableCellElement { return Dom.find( node, condition, root, undefined, undefined, withChild ? 'firstChild' : '' ); } static prevWithClass( node: HTMLElement, className: string ): HTMLElement | false { return <HTMLElement | false>this.prev( node, node => { return ( node && node.nodeType === Node.ELEMENT_NODE && (<HTMLElement>node).classList.contains(className) ); }, <HTMLElement>node.parentNode ); } static nextWithClass( node: HTMLElement, className: string ): HTMLElement | false { return <HTMLElement | false>this.next( node, node => { return ( node && node.nodeType === Node.ELEMENT_NODE && (<HTMLElement>node).classList.contains(className) ); }, <HTMLElement>node.parentNode ); } /** * Find next/prev node what `condition(next) === true` * * @param {Node} node * @param {function} condition * @param {Node} root * @param {boolean} [recurse=false] check first argument * @param {string} [sibling=nextSibling] nextSibling or previousSibling * @param {string|boolean} [child=firstChild] firstChild or lastChild * @return {Node|Boolean} */ static find( node: Node, condition: NodeCondition, root: HTMLElement | Node, recurse = false, sibling = 'nextSibling', child: string | false = 'firstChild' ): false | Node { if (recurse && condition(node)) { return node; } let start: Node | null = node, next: Node | null; do { next = (start as any)[sibling]; if (condition(next)) { return next ? next : false; } if (child && next && (next as any)[child]) { const nextOne: Node | false = Dom.find( (next as any)[child], condition, next, true, sibling, child ); if (nextOne) { return nextOne; } } if (!next) { next = start.parentNode; } start = next; } while (start && start !== root); return false; } /** * Find next/previous inline element * * @param node * @param toLeft * @param root */ static findInline = ( node: Node | null, toLeft: boolean, root: Node ): Node | null => { let prevElement: Node | null = node, nextElement: Node | null = null; do { if (prevElement) { nextElement = toLeft ? prevElement.previousSibling : prevElement.nextSibling; if ( !nextElement && prevElement.parentNode && prevElement.parentNode !== root && Dom.isInlineBlock(prevElement.parentNode) ) { prevElement = prevElement.parentNode; } else { break; } } else { break; } } while (!nextElement); while ( nextElement && Dom.isInlineBlock(nextElement) && (!toLeft ? nextElement.firstChild : nextElement.lastChild) ) { nextElement = !toLeft ? nextElement.firstChild : nextElement.lastChild; } return nextElement; // (nextElement !== root && Dom.isInlineBlock(nextElement)) ? nextElement : null; }; /** * Find next/prev node what `condition(next) === true` * * @param {Node} node * @param {function} condition * @param {Node} root * @param {string} [sibling=nextSibling] nextSibling or previousSibling * @param {string|boolean} [child=firstChild] firstChild or lastChild * @return {Node|Boolean} */ static findWithCurrent( node: Node, condition: NodeCondition, root: HTMLElement | Node, sibling: 'nextSibling' | 'previousSibling' = 'nextSibling', child: 'firstChild' | 'lastChild' = 'firstChild' ): false | Node { let next: Node | null = node; do { if (condition(next)) { return next ? next : false; } if (child && next && next[child]) { const nextOne: Node | false = Dom.findWithCurrent( next[child] as Node, 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 false; } /** * It goes through all the elements in ascending order, and checks to see if they meet the predetermined condition * * @param {callback} node * @param {function} condition * @param {Node} root Root element * @return {boolean|Node|HTMLElement|HTMLTableCellElement|HTMLTableElement} Return false if condition not be true */ static up( node: Node, condition: NodeCondition, root: Node ): false | Node | HTMLElement | HTMLTableCellElement | HTMLTableElement { let start = node; if (!node) { return false; } do { if (condition(start)) { return start; } if (start === root || !start.parentNode) { break; } start = start.parentNode; } while (start && start !== root); return false; } /** * Find parent by tag name * * @param {Node} node * @param {String|Function} tags * @param {HTMLElement} root * @return {Boolean|Node} */ static closest( node: Node, tags: string | NodeCondition | RegExp, root: HTMLElement ): Node | HTMLTableElement | HTMLElement | false | HTMLTableCellElement { let condition: NodeCondition; if (typeof tags === 'function') { condition = tags; } else if (tags instanceof RegExp) { condition = (tag: Node | null) => tag && tags.test(tag.nodeName); } else { condition = (tag: Node | null) => tag && new RegExp('^(' + tags + ')$', 'i').test(tag.nodeName); } return Dom.up(node, condition, root); } /** * Insert newElement after element * * @param elm * @param newElement */ static after(elm: HTMLElement, newElement: HTMLElement | DocumentFragment) { const parentNode: Node | null = elm.parentNode; if (!parentNode) { return; } if (parentNode.lastChild === elm) { parentNode.appendChild(newElement); } else { parentNode.insertBefore(newElement, elm.nextSibling); } } /** * Move all content to another element * * @param {Node} from * @param {Node} to * @param {boolean} inStart */ static moveContent(from: Node, to: Node, inStart: boolean = false) { const fragment: DocumentFragment = ( from.ownerDocument || document ).createDocumentFragment(); [].slice.call(from.childNodes).forEach((node: Node) => { if ( node.nodeType !== Node.TEXT_NODE || node.nodeValue !== consts.INVISIBLE_SPACE ) { fragment.appendChild(node); } }); if (!inStart || !to.firstChild) { to.appendChild(fragment); } else { to.insertBefore(fragment, to.firstChild); } } /** * Call callback condition function for all elements of node * * @param node * @param condition * @param prev */ static all( node: Node, condition: NodeCondition, prev: boolean = false ): Node | void { let nodes: Node[] = node.childNodes ? Array.prototype.slice.call(node.childNodes) : []; if (condition(node)) { return node; } if (prev) { nodes = nodes.reverse(); } nodes.forEach(child => { Dom.all(child, condition, prev); }); } /** * Check root contains child * * @param root * @param child * @return {boolean} */ static contains = (root: Node, child: Node): boolean => { while (child.parentNode) { if (child.parentNode === root) { return true; } child = child.parentNode; } return false; }; /** * Check root contains child or equal child * * @param {Node} root * @param {Node} child * @param {boolean} onlyContains * @return {boolean} */ static isOrContains = ( root: Node, child: Node, onlyContains: boolean = false ): boolean => { return ( child && root && ((root === child && !onlyContains) || Dom.contains(root, child)) ); }; /** * Safe remove element from DOM * * @param node */ static safeRemove(node: Node | false | null) { node && node.parentNode && node.parentNode.removeChild(node); } }