UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

214 lines (213 loc) 7.04 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 */ import { ViewComponent } from "../../core/component/index.js"; import { IS_PROD } from "../../core/constants.js"; import { Dom } from "../../core/dom/dom.js"; /** * Module for creating snapshot of editor which includes html content and the current selection */ export class Snapshot extends ViewComponent { constructor() { super(...arguments); this.__isBlocked = false; this.__levelOfTransaction = 0; } /** @override */ className() { return 'Snapshot'; } /** * Compare two snapshotes, if and htmls and selections match, then return true * * @param first - the first snapshote * @param second - second shot */ static equal(first, second) { return (first.html === second.html && JSON.stringify(first.range) === JSON.stringify(second.range)); } /** * Calc count element before some node in parentNode. All text nodes are joined */ static countNodesBeforeInParent(elm) { if (!elm.parentNode) { return 0; } const elms = elm.parentNode.childNodes; let count = 0, previous = null; for (let j = 0; j < elms.length; j += 1) { if (previous && !this.isIgnoredNode(elms[j]) && !(Dom.isText(previous) && Dom.isText(elms[j]))) { count += 1; } if (elms[j] === elm) { return count; } previous = elms[j]; } return 0; } /** * Calc normal offset in joined text nodes */ static strokeOffset(elm, offset) { while (Dom.isText(elm)) { elm = elm.previousSibling; if (Dom.isText(elm) && elm.nodeValue) { offset += elm.nodeValue.length; } } return offset; } /** * Calc whole hierarchy path before some element in editor's tree */ calcHierarchyLadder(elm) { const counts = []; if (!elm || !elm.parentNode || !Dom.isOrContains(this.j.editor, elm)) { return []; } while (elm && elm !== this.j.editor) { if (elm && !Snapshot.isIgnoredNode(elm)) { counts.push(Snapshot.countNodesBeforeInParent(elm)); } elm = elm.parentNode; } return counts.reverse(); } getElementByLadder(ladder) { let n = this.j.editor, i; for (i = 0; n && i < ladder.length; i += 1) { n = n.childNodes[ladder[i]]; } return n; } get isBlocked() { return this.__isBlocked; } __block(enable) { this.__isBlocked = enable; } transaction(changes) { this.__block(true); this.__levelOfTransaction += 1; try { changes(); } catch (e) { if (!IS_PROD) { throw e; } } finally { this.__levelOfTransaction -= 1; if (this.__levelOfTransaction === 0) { this.__block(false); } } } /** * Creates object a snapshot of editor: html and the current selection. Current selection calculate by * offset by start document * \{html: string, range: \{startContainer: int, startOffset: int, endContainer: int, endOffset: int\}\} or * \{html: string\} without selection */ make() { const snapshot = { html: '', range: { startContainer: [], startOffset: 0, endContainer: [], endOffset: 0 } }; snapshot.html = this.__getCleanedEditorValue(this.j.editor); const sel = this.j.s.sel; if (sel && sel.rangeCount) { const range = sel.getRangeAt(0); const startContainer = this.calcHierarchyLadder(range.startContainer); const endContainer = this.calcHierarchyLadder(range.endContainer); let startOffset = Snapshot.strokeOffset(range.startContainer, range.startOffset), endOffset = Snapshot.strokeOffset(range.endContainer, range.endOffset); if (!startContainer.length && range.startContainer !== this.j.editor) { startOffset = 0; } if (!endContainer.length && range.endContainer !== this.j.editor) { endOffset = 0; } snapshot.range = { startContainer, startOffset, endContainer, endOffset }; } return snapshot; } /** * Restores the state of the editor of the snapshot. Rebounding is not only html but selected text * * @param snapshot - snapshot of editor resulting from the `[[Snapshot.make]]` method * @see make */ restore(snapshot) { this.transaction(() => { const scroll = this.storeScrollState(); const html = this.__getCleanedEditorValue(this.j.editor); if (html !== snapshot.html) { this.j.value = snapshot.html; } this.restoreOnlySelection(snapshot); this.restoreScrollState(scroll); }); } storeScrollState() { return [this.j.ow.scrollY, this.j.editor.scrollTop]; } restoreScrollState(scrolls) { const { j } = this, { ow } = j; ow.scrollTo(ow.scrollX, scrolls[0]); j.editor.scrollTop = scrolls[1]; } /** * Restore selection from snapshot * * @param snapshot - snapshot of editor resulting from the [[Snapshot.make]] method * @see make */ restoreOnlySelection(snapshot) { try { if (snapshot.range) { const range = this.j.ed.createRange(); range.setStart(this.getElementByLadder(snapshot.range.startContainer), snapshot.range.startOffset); range.setEnd(this.getElementByLadder(snapshot.range.endContainer), snapshot.range.endOffset); this.j.s.selectRange(range); } } catch (__ignore) { this.j.editor.lastChild && this.j.s.setCursorAfter(this.j.editor.lastChild); if (!IS_PROD) { // tslint:disable-next-line:no-console console.warn('Broken snapshot', __ignore); } } } destruct() { this.__block(false); super.destruct(); } static isIgnoredNode(node) { return (Dom.isText(node) && !node.nodeValue) || Dom.isTemporary(node); } __getCleanedEditorValue(node) { const clone = node.cloneNode(true); Dom.temporaryList(clone).forEach(Dom.unwrap); return clone.innerHTML; } }