Files
evolution/data/webkit/e-undo-redo.js

940 lines
28 KiB
JavaScript

/*
* Copyright (C) 2019 Red Hat (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
/* semi-convention: private functions start with lower-case letter,
public functions start with upper-case letter. */
var EvoUndoRedo = {
E_UNDO_REDO_STATE_NONE : 0,
E_UNDO_REDO_STATE_CAN_UNDO : 1 << 0,
E_UNDO_REDO_STATE_CAN_REDO : 1 << 1,
stack : {
// to not claim changes when none being made
state : -1,
undoOpType : "",
redoOpType : "",
maybeStateChanged : function() {
var undoRecord, redoRecord, undoAvailable, undoOpType, redoAvailable, redoOpType;
undoRecord = EvoUndoRedo.stack.getCurrentUndoRecord();
redoRecord = EvoUndoRedo.stack.getCurrentRedoRecord();
undoAvailable = undoRecord != null;
undoOpType = undoRecord ? undoRecord.opType : "";
redoAvailable = redoRecord != null;
redoOpType = redoRecord ? redoRecord.opType : "";
var state = EvoUndoRedo.E_UNDO_REDO_STATE_NONE;
if (undoAvailable) {
state |= EvoUndoRedo.E_UNDO_REDO_STATE_CAN_UNDO;
}
if (redoAvailable) {
state |= EvoUndoRedo.E_UNDO_REDO_STATE_CAN_REDO;
}
if (EvoUndoRedo.state != state ||
EvoUndoRedo.undoOpType != (undoAvailable ? undoOpType : "") ||
EvoUndoRedo.redoOpType != (redoAvailable ? redoOpType : "")) {
EvoUndoRedo.state = state;
EvoUndoRedo.undoOpType = (undoAvailable ? undoOpType : "");
EvoUndoRedo.redoOpType = (redoAvailable ? redoOpType : "");
var params = {};
params.state = EvoUndoRedo.state;
params.undoOpType = EvoUndoRedo.undoOpType;
params.redoOpType = EvoUndoRedo.redoOpType;
window.webkit.messageHandlers.undoRedoStateChanged.postMessage(params);
}
},
MAX_DEPTH : 10000 + 1, /* it's one item less, due to the 'bottom' being always ignored */
array : [],
bottom : 0,
top : 0,
current : 0,
clampIndex : function(index) {
index = (index) % EvoUndoRedo.stack.MAX_DEPTH;
if (index < 0)
index += EvoUndoRedo.stack.MAX_DEPTH;
return index;
},
/* Returns currently active record for Undo operation, or null */
getCurrentUndoRecord : function() {
if (EvoUndoRedo.stack.current == EvoUndoRedo.stack.bottom || !EvoUndoRedo.stack.array.length ||
EvoUndoRedo.stack.current < 0 || EvoUndoRedo.stack.current > EvoUndoRedo.stack.array.length) {
return null;
}
return EvoUndoRedo.stack.array[EvoUndoRedo.stack.current];
},
/* Returns currently active record for Redo operation, or null */
getCurrentRedoRecord : function() {
if (EvoUndoRedo.stack.current == EvoUndoRedo.stack.top) {
return null;
}
var idx = EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current + 1);
if (idx < 0 || idx > EvoUndoRedo.stack.array.length) {
return null;
}
return EvoUndoRedo.stack.array[idx];
},
/* Clears the undo stack */
clear : function() {
EvoUndoRedo.stack.array.length = 0;
EvoUndoRedo.stack.bottom = 0;
EvoUndoRedo.stack.top = 0;
EvoUndoRedo.stack.current = 0;
EvoUndoRedo.stack.maybeStateChanged();
},
/* Adds a new record into the stack; if any undo had been made, then
those records are freed. It can also overwrite old undo steps, if
the stack size would overflow MAX_DEPTH. */
push : function(record) {
if (!EvoUndoRedo.stack.array.length) {
EvoUndoRedo.stack.array[0] = null;
}
var next = EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current + 1);
if (EvoUndoRedo.stack.current != EvoUndoRedo.stack.top) {
var tt, bb, cc;
tt = EvoUndoRedo.stack.top;
bb = EvoUndoRedo.stack.bottom;
cc = EvoUndoRedo.stack.current;
if (bb > tt) {
tt += EvoUndoRedo.stack.MAX_DEPTH;
cc += EvoUndoRedo.stack.MAX_DEPTH;
}
while (cc + 1 <= tt) {
EvoUndoRedo.stack.array[EvoUndoRedo.stack.clampIndex(cc + 1)] = null;
cc++;
}
}
if (next == EvoUndoRedo.stack.bottom) {
EvoUndoRedo.stack.bottom = EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.bottom + 1);
EvoUndoRedo.stack.array[EvoUndoRedo.stack.bottom] = null;
}
EvoUndoRedo.stack.current = next;
EvoUndoRedo.stack.top = next;
EvoUndoRedo.stack.array[next] = record;
EvoUndoRedo.stack.maybeStateChanged();
},
/* Moves the 'current' index in the stack and returns the undo record
to be undone; or 'null', when there's no undo record available. */
undo : function() {
var record = EvoUndoRedo.stack.getCurrentUndoRecord();
if (record) {
EvoUndoRedo.stack.current = EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current - 1);
}
EvoUndoRedo.stack.maybeStateChanged();
return record;
},
/* Moves the 'current' index in the stack and returns the redo record
to be redone; or 'null', when there's no redo record available. */
redo : function() {
var record = EvoUndoRedo.stack.getCurrentRedoRecord();
if (record) {
EvoUndoRedo.stack.current = EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current + 1);
}
EvoUndoRedo.stack.maybeStateChanged();
return record;
},
pathMatches : function(path1, path2) {
if (!path1)
return !path2;
else if (!path2 || path1.length != path2.length)
return false;
var ii;
for (ii = 0; ii < path1.length; ii++) {
if (path1[ii] != path2[ii])
return false;
}
return true;
},
topInsertTextAtSamePlace : function() {
if (EvoUndoRedo.stack.current != EvoUndoRedo.stack.top ||
EvoUndoRedo.stack.current == EvoUndoRedo.stack.bottom) {
return false;
}
var curr, prev;
curr = EvoUndoRedo.stack.array[EvoUndoRedo.stack.current];
prev = EvoUndoRedo.stack.array[EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current - 1)];
return curr && prev &&
curr.kind == EvoUndoRedo.RECORD_KIND_EVENT &&
curr.opType == "insertText" &&
!curr.selectionBefore.focusElem &&
prev.kind == EvoUndoRedo.RECORD_KIND_EVENT &&
prev.opType == "insertText" &&
!prev.selectionBefore.focusElem &&
curr.firstChildIndex == prev.firstChildIndex &&
curr.restChildrenCount == prev.restChildrenCount &&
curr.selectionBefore.anchorOffset == prev.selectionAfter.anchorOffset &&
EvoUndoRedo.stack.pathMatches(curr.path, prev.path) &&
EvoUndoRedo.stack.pathMatches(curr.selectionBefore.anchorElem, prev.selectionAfter.anchorElem);
},
maybeMergeConsecutive : function(skipFirst, opType) {
if (EvoUndoRedo.stack.current != EvoUndoRedo.stack.top ||
EvoUndoRedo.stack.current == EvoUndoRedo.stack.bottom) {
return;
}
var ii, from, curr, keep = null;
from = EvoUndoRedo.stack.current;
curr = EvoUndoRedo.stack.array[from];
if (skipFirst) {
keep = curr;
from = EvoUndoRedo.stack.clampIndex(from - 1);
curr = EvoUndoRedo.stack.array[from];
}
if (!curr ||
curr.kind != EvoUndoRedo.RECORD_KIND_EVENT ||
curr.opType != opType ||
curr.selectionBefore.focusElem) {
return;
}
for (ii = EvoUndoRedo.stack.clampIndex(from - 1);
ii != EvoUndoRedo.stack.bottom;
ii = EvoUndoRedo.stack.clampIndex(ii - 1)) {
var prev;
prev = EvoUndoRedo.stack.array[ii];
if (prev.kind != EvoUndoRedo.RECORD_KIND_EVENT ||
prev.opType != opType ||
prev.selectionBefore.focusElem ||
curr.firstChildIndex != prev.firstChildIndex ||
curr.restChildrenCount != prev.restChildrenCount ||
curr.selectionBefore.anchorOffset != prev.selectionAfter.anchorOffset ||
!EvoUndoRedo.stack.pathMatches(curr.path, prev.path) ||
!EvoUndoRedo.stack.pathMatches(curr.selectionBefore.anchorElem, prev.selectionAfter.anchorElem)) {
break;
}
if (opType == "insertText")
prev.opType = opType + "::merged";
prev.selectionAfter = curr.selectionAfter;
prev.htmlAfter = curr.htmlAfter;
curr = prev;
EvoUndoRedo.stack.array[EvoUndoRedo.stack.clampIndex(ii + 1)] = keep;
if (keep) {
EvoUndoRedo.stack.array[EvoUndoRedo.stack.clampIndex(ii + 2)] = null;
}
EvoUndoRedo.stack.top = ii + (keep ? 1 : 0);
EvoUndoRedo.stack.current = EvoUndoRedo.stack.top;
}
EvoUndoRedo.stack.maybeStateChanged();
},
maybeMergeInsertText : function(skipFirst) {
EvoUndoRedo.stack.maybeMergeConsecutive(skipFirst, "insertText");
EvoUndoRedo.stack.maybeMergeConsecutive(skipFirst, "insertText::WordDelim");
},
maybeMergeDragDrop : function() {
if (EvoUndoRedo.stack.current != EvoUndoRedo.stack.top ||
EvoUndoRedo.stack.current == EvoUndoRedo.stack.bottom ||
EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current - 1) == EvoUndoRedo.stack.bottom) {
return;
}
var curr, prev;
curr = EvoUndoRedo.stack.array[EvoUndoRedo.stack.current];
prev = EvoUndoRedo.stack.array[EvoUndoRedo.stack.clampIndex(EvoUndoRedo.stack.current - 1)];
if (curr && prev &&
curr.kind == EvoUndoRedo.RECORD_KIND_EVENT &&
prev.kind == EvoUndoRedo.RECORD_KIND_EVENT &&
curr.opType == "insertFromDrop" &&
prev.opType == "deleteByDrag") {
EvoUndoRedo.GroupTopRecords(2, "dragDrop::merged");
}
}
},
RECORD_KIND_EVENT : 1, /* managed by EvoUndoRedo itself, in DOM events */
RECORD_KIND_DOCUMENT : 2, /* saving whole document */
RECORD_KIND_GROUP : 3, /* not saving anything, just grouping several records together */
RECORD_KIND_CUSTOM : 4, /* custom record */
/*
Record {
int kind; // RECORD_KIND_...
string opType; // operation type, like the one from oninput
Array path; // path to the common parent of the affteded elements
int firstChildIndex; // the index of the first children affeted/recorded
int restChildrenCount; // the Undo/Redo affects only some children, these are those which are unaffected after the path
Object selectionBefore; // stored selection as it was before the change
string htmlBefore; // affected children before the change; can be null, when inserting new nodes
Object selectionAfter; // stored selection as it was after the change
string htmlAfter; // affected children before the change; can be null, when removed old nodes
Array records; // nested records; can be null or undefined
}
The path, firstChildIndex and restChildrenCount together describe where the changes happened.
That is, for example when changing node 'b' into 'x' and 'y':
<body> | <body>
<a/> | <a/>
<b/> | <x/>
<c/> | <y/>
<d/> | <c/>
</body> | <d/>
| </body>
the 'path' points to 'body', the firstChildIndex=1 and restChildrenCount=2. Then undo/redo can
delete all nodes between index >= firstChildIndex && index < children.length - restChildrenCount.
*/
dropTarget : null, // passed from dropCb() into beforeInputCb()/inputCb() for "insertFromDrop" event
disabled : 0,
ongoingRecordings : [] // the recordings can be nested
};
EvoUndoRedo.Attach = function()
{
if (document.documentElement) {
document.documentElement.onbeforeinput = EvoUndoRedo.beforeInputCb;
document.documentElement.oninput = EvoUndoRedo.inputCb;
document.documentElement.ondrop = EvoUndoRedo.dropCb;
}
}
EvoUndoRedo.Detach = function()
{
if (document.documentElement) {
document.documentElement.onbeforeinput = null;
document.documentElement.oninput = null;
document.documentElement.ondrop = null;
}
}
EvoUndoRedo.Enable = function()
{
if (!EvoUndoRedo.disabled) {
throw "EvoUndoRedo:: Cannot Enable, when not disabled";
}
EvoUndoRedo.disabled--;
}
EvoUndoRedo.Disable = function()
{
EvoUndoRedo.disabled++;
if (!EvoUndoRedo.disabled) {
throw "EvoUndoRedo:: Overflow in Disable";
}
}
EvoUndoRedo.isWordDelimEvent = function(inputEvent)
{
return inputEvent.inputType == "insertText" &&
inputEvent.data &&
inputEvent.data.length == 1 &&
(inputEvent.data == " " || inputEvent.data == "\t");
}
EvoUndoRedo.beforeInputCb = function(inputEvent)
{
if (EvoUndoRedo.disabled) {
return;
}
var opType = inputEvent.inputType, record, startNode = null, endNode = null;
if (EvoUndoRedo.isWordDelimEvent(inputEvent))
opType += "::WordDelim";
if (opType == "insertFromDrop")
startNode = EvoUndoRedo.dropTarget;
if (document.getSelection().isCollapsed) {
if (opType == "deleteWordBackward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "backward", "word");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteWordForward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "forward", "word");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteSoftLineBackward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "backward", "line");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteSoftLineForward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "forward", "line");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteEntireSoftLine") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "backward", "line");
startNode = document.getSelection().anchorNode;
document.getSelection().modify("move", "forward", "line");
endNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteHardLineBackward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "backward", "paragraph");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteHardLineForward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "forward", "paragraph");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteContentBackward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "backward", "paragraph");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
} else if (opType == "deleteContentForward") {
var sel = EvoSelection.Store(document);
document.getSelection().modify("move", "forward", "paragraph");
startNode = document.getSelection().anchorNode;
EvoSelection.Restore(document, sel);
}
}
record = EvoUndoRedo.StartRecord(EvoUndoRedo.RECORD_KIND_EVENT, opType, startNode, endNode,
EvoEditor.CLAIM_CONTENT_FLAG_SAVE_HTML | EvoEditor.CLAIM_CONTENT_FLAG_USE_PARENT_BLOCK_NODE);
/* Changing format with collapsed selection doesn't change HTML structure immediately */
if (record && opType.startsWith("format") && document.getSelection().isCollapsed) {
record.ignore = true;
}
}
EvoUndoRedo.inputCb = function(inputEvent)
{
var isWordDelim = EvoUndoRedo.isWordDelimEvent(inputEvent);
if (EvoUndoRedo.disabled) {
EvoEditor.EmitContentChanged();
EvoEditor.AfterInputEvent(inputEvent, isWordDelim);
return;
}
var opType = inputEvent.inputType;
if (isWordDelim)
opType += "::WordDelim";
if (EvoUndoRedo.StopRecord(EvoUndoRedo.RECORD_KIND_EVENT, opType)) {
EvoEditor.EmitContentChanged();
EvoEditor.maybeUpdateFormattingState(EvoEditor.FORCE_MAYBE);
}
if (!EvoUndoRedo.ongoingRecordings.length && opType == "insertText" &&
!EvoUndoRedo.stack.topInsertTextAtSamePlace()) {
EvoUndoRedo.stack.maybeMergeInsertText(true);
}
EvoEditor.forceFormatStateUpdate = EvoEditor.forceFormatStateUpdate || opType == "" || opType.startsWith("format");
if (opType == "insertFromDrop") {
EvoUndoRedo.dropTarget = null;
EvoUndoRedo.stack.maybeMergeDragDrop();
}
EvoEditor.AfterInputEvent(inputEvent, isWordDelim);
}
EvoUndoRedo.dropCb = function(event)
{
EvoUndoRedo.dropTarget = event.toElement;
}
EvoUndoRedo.applyRecord = function(record, isUndo, withSelection)
{
if (!record) {
return;
}
var kind = record.kind;
if (kind == EvoUndoRedo.RECORD_KIND_GROUP) {
var ii, records;
records = record.records;
if (records && records.length) {
for (ii = 0; ii < records.length; ii++) {
EvoUndoRedo.applyRecord(records[isUndo ? (records.length - ii - 1) : ii], isUndo, false);
}
}
if (withSelection) {
EvoSelection.Restore(document, isUndo ? record.selectionBefore : record.selectionAfter);
}
return;
}
EvoUndoRedo.Disable();
try {
if (kind == EvoUndoRedo.RECORD_KIND_DOCUMENT) {
if (isUndo) {
document.documentElement.innerHTML = record.htmlBefore;
} else {
document.documentElement.innerHTML = record.htmlAfter;
}
if (record.apply != null) {
record.apply(record, isUndo);
}
} else if (kind == EvoUndoRedo.RECORD_KIND_CUSTOM && record.apply != null) {
record.apply(record, isUndo);
} else {
var commonParent;
commonParent = EvoSelection.FindElementByPath(document.body, record.path);
if (!commonParent) {
throw "EvoUndoRedo::applyRecord: Cannot find parent at path " + record.path;
}
EvoUndoRedo.RestoreChildren(record, commonParent, isUndo);
}
if (withSelection) {
EvoSelection.Restore(document, isUndo ? record.selectionBefore : record.selectionAfter);
}
} finally {
EvoUndoRedo.Enable();
}
}
EvoUndoRedo.StartRecord = function(kind, opType, startNode, endNode, flags)
{
if (EvoUndoRedo.disabled) {
return null;
}
var record = {};
record.kind = kind;
record.opType = opType;
record.selectionBefore = EvoSelection.Store(document);
if (kind == EvoUndoRedo.RECORD_KIND_DOCUMENT) {
record.htmlBefore = document.documentElement.innerHTML;
} else if (kind != EvoUndoRedo.RECORD_KIND_GROUP) {
var affected;
affected = EvoEditor.ClaimAffectedContent(startNode, endNode, flags);
record.path = affected.path;
record.firstChildIndex = affected.firstChildIndex;
record.restChildrenCount = affected.restChildrenCount;
if ((flags & EvoEditor.CLAIM_CONTENT_FLAG_SAVE_HTML) != 0)
record.htmlBefore = affected.html;
}
EvoUndoRedo.ongoingRecordings[EvoUndoRedo.ongoingRecordings.length] = record;
return record;
}
EvoUndoRedo.StopRecord = function(kind, opType)
{
if (EvoUndoRedo.disabled) {
return false;
}
if (!EvoUndoRedo.ongoingRecordings.length) {
// Workaround WebKitGTK+ bug not sending beforeInput event when deleting with backspace
// https://bugs.webkit.org/show_bug.cgi?id=206341
if (opType == "deleteContentBackward")
return false;
throw "EvoUndoRedo:StopRecord: Nothing is recorded for kind:" + kind + " opType:'" + opType + "'";
}
var record = EvoUndoRedo.ongoingRecordings[EvoUndoRedo.ongoingRecordings.length - 1];
// Events can overlap sometimes, like when doing drag&drop inside web view
if (record.kind != kind || record.opType != opType) {
var ii;
for (ii = EvoUndoRedo.ongoingRecordings.length - 2; ii >= 0; ii--) {
record = EvoUndoRedo.ongoingRecordings[ii];
if (record.kind == kind && record.opType == opType) {
var jj;
for (jj = ii + 1; jj < EvoUndoRedo.ongoingRecordings.length; jj++) {
EvoUndoRedo.ongoingRecordings[jj - 1] = EvoUndoRedo.ongoingRecordings[jj];
}
EvoUndoRedo.ongoingRecordings[EvoUndoRedo.ongoingRecordings.length - 1] = record;
break;
}
}
}
if (record.kind != kind || record.opType != opType) {
// The "InsertContent", especially when inserting plain text, can receive multiple 'input' events
// with "insertParagraph", "insertText" and similar, which do not have its counterpart beforeInput event,
// thus ignore those
if (record.opType == "InsertContent" && opType.startsWith("insert"))
return;
throw "EvoUndoRedo:StopRecord: Mismatch in record kind, expected " + record.kind + " (" + record.opType + "), but received " +
kind + "(" + opType + "); ongoing recordings:" + EvoUndoRedo.ongoingRecordings.length;
}
EvoUndoRedo.ongoingRecordings.length = EvoUndoRedo.ongoingRecordings.length - 1;
// ignore empty group records
if (kind == EvoUndoRedo.RECORD_KIND_GROUP && (!record.records || !record.records.length)) {
record.ignore = true;
}
if (record.ignore) {
if (!EvoUndoRedo.ongoingRecordings.length &&
(record.kind != EvoUndoRedo.RECORD_KIND_EVENT || record.opType != "insertText")) {
EvoUndoRedo.stack.maybeMergeInsertText(false);
}
return false;
}
if (kind == EvoUndoRedo.RECORD_KIND_DOCUMENT) {
record.htmlAfter = document.documentElement.innerHTML;
} else if (record.htmlBefore != window.undefined) {
var commonParent;
commonParent = EvoSelection.FindElementByPath(document.body, record.path);
if (!commonParent) {
throw "EvoUndoRedo.StopRecord:: Failed to stop '" + opType + "', cannot find common parent";
}
EvoUndoRedo.BackupChildrenAfter(record, commonParent);
// some formatting commands do not change HTML structure immediately, thus ignore those
if (kind == EvoUndoRedo.RECORD_KIND_EVENT && record.htmlBefore == record.htmlAfter) {
if (!EvoUndoRedo.ongoingRecordings.length && record.opType != "insertText") {
EvoUndoRedo.stack.maybeMergeInsertText(false);
}
return false;
}
}
record.selectionAfter = EvoSelection.Store(document);
if (EvoUndoRedo.ongoingRecordings.length && EvoUndoRedo.ongoingRecordings[EvoUndoRedo.ongoingRecordings.length - 1].kind == EvoUndoRedo.RECORD_KIND_GROUP) {
var parentRecord = EvoUndoRedo.ongoingRecordings[EvoUndoRedo.ongoingRecordings.length - 1];
var records = parentRecord.records;
if (!records) {
records = [];
}
records[records.length] = record;
parentRecord.records = records;
} else {
EvoUndoRedo.stack.push(record);
if (record.kind == EvoUndoRedo.RECORD_KIND_EVENT && record.opType == "insertText::WordDelim") {
EvoUndoRedo.stack.maybeMergeConsecutive(true, "insertText");
EvoUndoRedo.stack.maybeMergeConsecutive(false, "insertText::WordDelim");
} else if (record.kind != EvoUndoRedo.RECORD_KIND_EVENT || record.opType != "insertText") {
EvoUndoRedo.stack.maybeMergeInsertText(true);
}
}
return true;
}
EvoUndoRedo.IsRecording = function()
{
return !EvoUndoRedo.disabled && EvoUndoRedo.ongoingRecordings.length > 0;
}
EvoUndoRedo.Undo = function()
{
var record = EvoUndoRedo.stack.undo();
if (!record)
return;
EvoUndoRedo.applyRecord(record, true, true);
EvoEditor.maybeUpdateFormattingState(EvoEditor.FORCE_YES);
EvoEditor.EmitContentChanged();
}
EvoUndoRedo.Redo = function()
{
var record = EvoUndoRedo.stack.redo();
if (!record)
return;
EvoUndoRedo.applyRecord(record, false, true);
EvoEditor.maybeUpdateFormattingState(EvoEditor.FORCE_YES);
EvoEditor.EmitContentChanged();
}
EvoUndoRedo.Clear = function()
{
EvoUndoRedo.stack.clear();
}
EvoUndoRedo.GroupTopRecords = function(nRecords, opType)
{
if (EvoUndoRedo.disabled)
return;
if (EvoUndoRedo.ongoingRecordings.length)
throw "EvoUndoRedo.GroupTopRecords: Cannot be called when there are ongoing recordings";
var group = {};
group.kind = EvoUndoRedo.RECORD_KIND_GROUP;
group.opType = opType;
group.selectionBefore = null;
group.selectionAfter = null;
group.records = [];
while (nRecords >= 1) {
nRecords--;
var record = EvoUndoRedo.stack.undo();
if (!record)
break;
group.records[group.records.length] = record;
group.selectionBefore = record.selectionBefore;
if (!group.selectionAfter)
group.selectionAfter = record.selectionAfter;
if (!group.opType)
group.opType = record.opType + "::Grouped";
}
if (group.records.length) {
group.records = group.records.reverse();
EvoUndoRedo.stack.push(group);
}
EvoUndoRedo.stack.maybeStateChanged();
}
/* Backs up all the children elements between firstChildIndex and lastChildIndex inclusive,
saving their HTML content into record.htmlBefore; it stores only element children,
not text or other nodes. Use also EvoUndoRedo.BackupChildrenAfter() to save all data
needed by EvoUndoRedo.RestoreChildren(), which is used to restore saved data.
The firstChildIndex can be -1, to back up parent's innerHTML.
*/
EvoUndoRedo.BackupChildrenBefore = function(record, parent, firstChildIndex, lastChildIndex)
{
var currentElemsArray = EvoEditor.RemoveCurrentElementAttr();
try {
record.firstChildIndex = firstChildIndex;
if (firstChildIndex == -1) {
record.htmlBefore = parent.innerHTML;
} else {
record.htmlBefore = "";
var ii;
for (ii = firstChildIndex; ii < parent.children.length; ii++) {
record.htmlBefore += parent.children[ii].outerHTML;
if (ii == lastChildIndex) {
ii++;
break;
}
}
record.restChildrenCount = parent.children.length - ii;
}
} finally {
EvoEditor.RestoreCurrentElementAttr(currentElemsArray);
}
}
EvoUndoRedo.BackupChildrenAfter = function(record, parent)
{
if (record.firstChildIndex == undefined)
throw "EvoUndoRedo.BackupChildrenAfter: 'record' doesn't contain 'firstChildIndex' property";
if (record.firstChildIndex != -1 && record.restChildrenCount == undefined)
throw "EvoUndoRedo.BackupChildrenAfter: 'record' doesn't contain 'restChildrenCount' property";
if (record.htmlBefore == undefined)
throw "EvoUndoRedo.BackupChildrenAfter: 'record' doesn't contain 'htmlBefore' property";
var currentElemsArray = EvoEditor.RemoveCurrentElementAttr();
try {
if (record.firstChildIndex == -1) {
record.htmlAfter = parent.innerHTML;
} else {
record.htmlAfter = "";
var ii, first, last;
first = record.firstChildIndex;
// it can equal to the children.length, when the node had been removed
if (first < 0 || first > parent.children.length) {
throw "EvoUndoRedo.BackupChildrenAfter: firstChildIndex (" + first + ") out of bounds (" + parent.children.length + ")";
}
last = parent.children.length - record.restChildrenCount;
if (last < 0 || last < first) {
throw "EvoUndoRedo::BackupChildrenAfter: restChildrenCount (" + record.restChildrenCount + ") out of bounds (length:" +
parent.children.length + " first:" + first + " last:" + last + ")";
}
for (ii = first; ii < last; ii++) {
if (ii >= 0 && ii < parent.children.length) {
record.htmlAfter += parent.children[ii].outerHTML;
}
}
}
} finally {
EvoEditor.RestoreCurrentElementAttr(currentElemsArray);
}
}
// restores content of 'parent' based on the information saved by EvoUndoRedo.BackupChildrenBefore()
// and EvoUndoRedo.BackupChildrenAfter()
EvoUndoRedo.RestoreChildren = function(record, parent, isUndo)
{
var first, last, ii;
if (record.firstChildIndex == undefined)
throw "EvoUndoRedo.RestoreChildren: 'record' doesn't contain 'firstChildIndex' property";
if (record.firstChildIndex != -1 && record.restChildrenCount == undefined)
throw "EvoUndoRedo.RestoreChildren: 'record' doesn't contain 'restChildrenCount' property";
if (record.htmlBefore == undefined)
throw "EvoUndoRedo.RestoreChildren: 'record' doesn't contain 'htmlBefore' property";
if (record.htmlAfter == undefined)
throw "EvoUndoRedo.RestoreChildren: 'record' doesn't contain 'htmlAfter' property";
first = record.firstChildIndex;
if (first == -1) {
if (isUndo) {
parent.innerHTML = record.htmlBefore;
} else {
parent.innerHTML = record.htmlAfter;
}
} else {
// it can equal to the children.length, when the node had been removed
if (first < 0 || first > parent.children.length) {
throw "EvoUndoRedo::RestoreChildren: firstChildIndex (" + first + ") out of bounds (" + parent.children.length + ")";
}
last = parent.children.length - record.restChildrenCount;
if (last < 0 || last < first) {
throw "EvoUndoRedo::RestoreChildren: restChildrenCount (" + record.restChildrenCount + ") out of bounds (length:" +
parent.children.length + " first:" + first + " last:" + last + ")";
}
for (ii = last - 1; ii >= first; ii--) {
if (ii >= 0 && ii < parent.children.length) {
parent.removeChild(parent.children[ii]);
}
}
var tmpNode = document.createElement("evo-tmp");
if (isUndo) {
tmpNode.innerHTML = record.htmlBefore;
} else {
tmpNode.innerHTML = record.htmlAfter;
}
if (first < parent.children.length) {
first = parent.children[first];
while(tmpNode.firstElementChild) {
parent.insertBefore(tmpNode.firstElementChild, first);
}
} else {
while(tmpNode.firstElementChild) {
parent.appendChild(tmpNode.firstElementChild);
}
}
}
}
EvoUndoRedo.Attach();