@syncfusion/ej2-richtexteditor
Version:
Essential JS 2 RichTextEditor component
324 lines (317 loc) • 13 kB
JavaScript
import { debounce, isNullOrUndefined, detach, removeClass } from '@syncfusion/ej2-base';
import { NodeSelection } from './../../selection/selection';
import * as EVENTS from './../../common/constant';
import { isIDevice, scrollToCursor, setEditFrameFocus } from '../../common/util';
import { CLS_AUD_FOCUS, CLS_VID_FOCUS } from './../../common/constant';
/**
* `Undo` module is used to handle undo actions.
*/
var UndoRedoManager = /** @class */ (function () {
function UndoRedoManager(parent, options) {
this.undoRedoStack = [];
this.parent = parent;
this.undoRedoSteps = !isNullOrUndefined(options) ? options.undoRedoSteps : 30;
this.undoRedoTimer = !isNullOrUndefined(options) ? options.undoRedoTimer : 300;
this.addEventListener();
}
UndoRedoManager.prototype.addEventListener = function () {
this.debounceListener = debounce(this.keyUp, this.undoRedoTimer);
this.parent.observer.on(EVENTS.KEY_UP_HANDLER, this.debounceListener, this);
this.parent.observer.on(EVENTS.KEY_DOWN_HANDLER, this.keyDown, this);
this.parent.observer.on(EVENTS.ACTION, this.onAction, this);
this.parent.observer.on(EVENTS.MODEL_CHANGED_PLUGIN, this.onPropertyChanged, this);
this.parent.observer.on(EVENTS.INTERNAL_DESTROY, this.destroy, this);
};
UndoRedoManager.prototype.onPropertyChanged = function (props) {
for (var _i = 0, _a = Object.keys(props.newProp); _i < _a.length; _i++) {
var prop = _a[_i];
switch (prop) {
case 'undoRedoSteps':
this.undoRedoSteps = props.newProp.undoRedoSteps;
break;
case 'undoRedoTimer':
this.undoRedoTimer = props.newProp.undoRedoTimer;
break;
}
}
};
UndoRedoManager.prototype.removeEventListener = function () {
this.parent.observer.off(EVENTS.KEY_UP_HANDLER, this.keyUp);
this.parent.observer.off(EVENTS.KEY_DOWN_HANDLER, this.keyDown);
this.parent.observer.off(EVENTS.ACTION, this.onAction);
this.parent.observer.off(EVENTS.MODEL_CHANGED_PLUGIN, this.onPropertyChanged);
this.parent.observer.off(EVENTS.INTERNAL_DESTROY, this.destroy);
this.debounceListener = null;
};
/**
* onAction method
*
* @param {IHtmlSubCommands} e - specifies the sub command
* @returns {void}
* @hidden
*/
UndoRedoManager.prototype.onAction = function (e) {
if (e.subCommand === 'Undo') {
this.undo(e);
}
else {
this.redo(e);
}
};
/**
* Destroys the ToolBar.
*
* @function destroy
* @returns {void}
* @hidden
*/
UndoRedoManager.prototype.destroy = function () {
this.removeEventListener();
this.element = null;
this.steps = null;
this.undoRedoStack = [];
this.undoRedoSteps = null;
this.undoRedoTimer = null;
};
UndoRedoManager.prototype.keyDown = function (e) {
var event = e.event;
// eslint-disable-next-line
var proxy = this;
switch (event.action) {
case 'undo':
event.preventDefault();
proxy.undo(e);
break;
case 'redo':
event.preventDefault();
proxy.redo(e);
break;
}
};
UndoRedoManager.prototype.keyUp = function (e) {
if (e.event.keyCode !== 17 && !e.event.ctrlKey) {
this.saveData(e);
}
};
UndoRedoManager.prototype.getTextContentFromFragment = function (fragment) {
var textContent = '';
for (var i = 0; i < fragment.childNodes.length; i++) {
var childNode = fragment.childNodes[i];
if (childNode.nodeType === Node.TEXT_NODE) {
textContent += childNode.textContent;
}
else if (childNode.nodeType === Node.ELEMENT_NODE) {
textContent += this.getTextContentFromFragment(childNode);
}
}
return textContent;
};
UndoRedoManager.prototype.isElementStructureEqual = function (previousFragment, currentFragment) {
if (previousFragment.childNodes.length !== currentFragment.childNodes.length) {
return false;
}
for (var i = 0; i < previousFragment.childNodes.length; i++) {
var previousFragmentNode = previousFragment.childNodes[i];
var currentFragmentNode = currentFragment.childNodes[i];
if (!previousFragmentNode || !currentFragmentNode) {
return false;
}
if (previousFragmentNode.nodeType !== currentFragmentNode.nodeType) {
return false;
}
if (previousFragmentNode.outerHTML !== currentFragmentNode.outerHTML) {
return false;
}
}
return true;
};
/**
* RTE collection stored html format.
*
* @function saveData
* @param {KeyboardEvent} e - specifies the keyboard event
* @returns {void}
* @hidden
*/
UndoRedoManager.prototype.saveData = function (e) {
if (!this.parent.currentDocument) {
return;
}
var range = new NodeSelection(this.parent.editableElement).getRange(this.parent.currentDocument);
var currentContainer = this.parent.editableElement === range.startContainer.parentElement ?
range.startContainer.parentElement : range.startContainer;
for (var i = currentContainer.childNodes.length - 1; i >= 0; i--) {
if (!isNullOrUndefined(currentContainer.childNodes[i]) && currentContainer.childNodes[i].nodeName === '#text' &&
currentContainer.childNodes[i].textContent.length === 0 && currentContainer.childNodes[i].nodeName !== 'IMG' &&
currentContainer.childNodes[i].nodeName !== 'BR' && currentContainer.childNodes[i].nodeName && 'HR') {
detach(currentContainer.childNodes[i]);
}
}
range = new NodeSelection(this.parent.editableElement).getRange(this.parent.currentDocument);
var save = new NodeSelection(this.parent.editableElement).save(range, this.parent.currentDocument);
var clonedElement = this.removeResizeElement(this.parent.editableElement.cloneNode(true));
var fragment = document.createDocumentFragment();
while (clonedElement.firstChild) {
fragment.appendChild(clonedElement.firstChild);
}
var changEle = { text: fragment, range: save };
if (this.undoRedoStack.length >= this.steps) {
this.undoRedoStack = this.undoRedoStack.slice(0, this.steps + 1);
}
if (this.undoRedoStack.length > 1 && (this.undoRedoStack[this.undoRedoStack.length - 1].range.range.collapsed === range.collapsed)
&& (this.undoRedoStack[this.undoRedoStack.length - 1].range.startOffset === save.range.startOffset) &&
(this.undoRedoStack[this.undoRedoStack.length - 1].range.endOffset === save.range.endOffset) &&
(this.undoRedoStack[this.undoRedoStack.length - 1].range.range.startContainer === save.range.startContainer) &&
(this.getTextContentFromFragment(this.undoRedoStack[this.undoRedoStack.length - 1].text).trim() ===
this.getTextContentFromFragment(changEle.text).trim()) &&
this.isElementStructureEqual(this.undoRedoStack[this.undoRedoStack.length - 1].text, changEle.text)) {
return;
}
this.undoRedoStack.push(changEle);
this.steps = this.undoRedoStack.length - 1;
if (this.steps > this.undoRedoSteps) {
this.undoRedoStack.shift();
this.steps--;
}
if (e && e.callBack) {
e.callBack();
}
};
/*
* Cleans up an HTML element by removing all resize handles and visual focus indicators
* related to video, image, and audio editing UI.
*
* - Removes the DOM elements used for resizing videos and images.
* - Clears visual focus classes and outline styles added for video, image, and audio elements.
*/
UndoRedoManager.prototype.removeResizeElement = function (element) {
// Remove video resize element
var videoResize = element.querySelector('.e-vid-resize');
if (videoResize) {
detach(videoResize);
}
// Remove video focus class
var videoFocus = element.querySelector("." + CLS_VID_FOCUS);
if (videoFocus) {
removeClass([videoFocus], [CLS_VID_FOCUS, 'e-resize']);
}
// Remove image resize element
var imageResize = element.querySelector('.e-img-resize');
if (imageResize) {
detach(imageResize);
}
// Remove focus/resize classes from all images
var images = element.querySelectorAll('img');
for (var i = 0; i < images.length; i++) {
var img = images[i];
removeClass([img], ['e-img-focus', 'e-resize']);
}
// Remove audio focus class
var audioFocus = element.querySelector("." + CLS_AUD_FOCUS);
if (audioFocus) {
removeClass([audioFocus], [CLS_AUD_FOCUS]);
}
// Remove outline from images, audio, and video elements
var outlineElements = element.querySelectorAll('img, audio, video');
for (var i = 0; i < outlineElements.length; i++) {
var outlineElem = outlineElements[i];
outlineElem.style.outline = '';
}
return element;
};
/**
* Undo the editable text.
*
* @function undo
* @param {IHtmlSubCommands} e - specifies the sub commands
* @returns {void}
* @hidden
*/
UndoRedoManager.prototype.undo = function (e) {
if (this.steps > 0) {
var range = this.undoRedoStack[this.steps - 1].range;
var removedContent = this.undoRedoStack[this.steps - 1].text;
this.parent.editableElement.innerHTML = '';
this.parent.editableElement.appendChild(removedContent.cloneNode(true));
this.parent.editableElement.focus();
scrollToCursor(this.parent.currentDocument, this.parent.editableElement);
if (isIDevice()) {
setEditFrameFocus(this.parent.editableElement, e.selector);
}
range.restore();
this.steps--;
if (e.callBack) {
e.callBack({
requestType: 'Undo',
editorMode: 'HTML',
range: range,
elements: this.parent.nodeSelection.getSelectedNodes(this.parent.currentDocument),
event: e.event
});
}
}
};
/**
* Redo the editable text.
*
* @param {IHtmlSubCommands} e - specifies the sub commands
* @function redo
* @returns {void}
* @hidden
*/
UndoRedoManager.prototype.redo = function (e) {
if (this.undoRedoStack[this.steps + 1] != null) {
var range = this.undoRedoStack[this.steps + 1].range;
var addedContent = this.undoRedoStack[this.steps + 1].text;
this.parent.editableElement.innerHTML = '';
this.parent.editableElement.appendChild(addedContent.cloneNode(true));
this.parent.editableElement.focus();
scrollToCursor(this.parent.currentDocument, this.parent.editableElement);
if (isIDevice()) {
setEditFrameFocus(this.parent.editableElement, e.selector);
}
range.restore();
this.steps++;
if (e.callBack) {
e.callBack({
requestType: 'Redo',
editorMode: 'HTML',
range: range,
elements: this.parent.nodeSelection.getSelectedNodes(this.parent.currentDocument),
event: e.event
});
}
}
};
/**
* getUndoStatus method
*
* @returns {boolean} - returns the boolean value
* @hidden
*/
UndoRedoManager.prototype.getUndoStatus = function () {
var status = { undo: false, redo: false };
if (this.steps > 0) {
status.undo = true;
}
if (this.undoRedoStack[this.steps + 1] != null) {
status.redo = true;
}
return status;
};
UndoRedoManager.prototype.getCurrentStackIndex = function () {
return this.steps;
};
/**
* Clears the undo and redo stacks and reset the steps to null..
*
* @returns {void}
* @public
*/
UndoRedoManager.prototype.clear = function () {
this.undoRedoStack = [];
this.steps = null;
};
return UndoRedoManager;
}());
export { UndoRedoManager };