jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
236 lines (198 loc) • 5.38 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { IJodit, SnapshotType } from '../../types';
import { ViewComponent } from '../../core/component';
import { Dom } from '../../core/dom';
/**
* Module for creating snapshot of editor which includes html content and the current selection
*/
export class Snapshot extends ViewComponent<IJodit> {
/**
* Compare two snapshotes, if and htmls and selections match, then return true
*
* @param {SnapshotType} first - the first snapshote
* @param {SnapshotType} second - second shot
* @return {boolean}
*/
static equal(first: SnapshotType, second: SnapshotType): boolean {
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
*
* @param {Node | null} elm
* @return {number}
*/
private static countNodesBeforeInParent(elm: Node): number {
if (!elm.parentNode) {
return 0;
}
const elms: NodeList = elm.parentNode.childNodes;
let count: number = 0,
last: Node | null = null,
j: number;
for (j = 0; j < elms.length; j += 1) {
if (
last &&
!(Dom.isText(elms[j]) && elms[j].textContent === '') &&
!(Dom.isText(last) && Dom.isText(elms[j]))
) {
count += 1;
}
if (elms[j] === elm) {
return count;
}
last = elms[j];
}
return 0;
}
/**
* Calc normal offset in joined text nodes
*
* @param {Node | null} elm
* @param {number} offset
* @return {number}
*/
private static strokeOffset(elm: Node | null, offset: number): number {
while (Dom.isText(elm)) {
elm = elm.previousSibling;
if (Dom.isText(elm) && elm.textContent !== null) {
offset += elm.textContent.length;
}
}
return offset;
}
/**
* Calc whole hierarchy path before some element in editor's tree
*
* @param {Node | null} elm
* @return {number[]}
* @private
*/
private calcHierarchyLadder(elm: Node | null): number[] {
const counts: number[] = [];
if (!elm || !elm.parentNode || !Dom.isOrContains(this.j.editor, elm)) {
return [];
}
while (elm && elm !== this.j.editor) {
if (elm) {
counts.push(Snapshot.countNodesBeforeInParent(elm));
}
elm = elm.parentNode;
}
return counts.reverse();
}
private getElementByLadder(ladder: number[]): Node {
let n: Node = this.j.editor as Node,
i: number;
for (i = 0; n && i < ladder.length; i += 1) {
n = n.childNodes[ladder[i]];
}
return n;
}
isBlocked: boolean = false;
/**
* Creates object a snapshot of editor: html and the current selection. Current selection calculate by
* offset by start document
*
* @return {object}
* {html: string, range: {startContainer: int, startOffset: int, endContainer: int, endOffset: int}} or
* {html: string} without selection
*/
make(): SnapshotType {
const snapshot: SnapshotType = {
html: '',
range: {
startContainer: [],
startOffset: 0,
endContainer: [],
endOffset: 0
}
};
snapshot.html = this.j.getNativeEditorValue();
const sel = this.j.s.sel;
if (sel && sel.rangeCount) {
const range = sel.getRangeAt(0),
startContainer = this.calcHierarchyLadder(range.startContainer),
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 {object} snapshot - snapshot of editor resulting from the `{@link Snapshot~make|make}`
* @see make
*/
restore(snapshot: SnapshotType): void {
this.isBlocked = true;
const value = this.j.getNativeEditorValue();
if (value !== snapshot.html) {
this.j.setEditorValue(snapshot.html);
}
this.restoreOnlySelection(snapshot);
this.isBlocked = false;
}
/**
* Restore selection from snapshot
*
* @param {object} snapshot - snapshot of editor resulting from the `{@link Snapshot~make|make}`
* @see make
*/
restoreOnlySelection(snapshot: SnapshotType): void {
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 (!isProd) {
// tslint:disable-next-line:no-console
console.warn('Broken snapshot', __ignore);
}
}
}
destruct(): void {
this.isBlocked = false;
super.destruct();
}
}