scribe-editor
Advanced tools
Comparing version 1.2.11 to 1.3.0
@@ -0,1 +1,5 @@ | ||
# 1.3.0 | ||
Introduces a new time-based undo manager and improvements to allow multiple Scribe instances to share or have a separate undo manager. Thanks to [Abdulrahman Alsaleh](https://github.com/aaalsaleh) for providing the code and spending a lot of time working with us on the tests. | ||
# 1.2.11 | ||
@@ -2,0 +6,0 @@ |
{ | ||
"name": "scribe-editor", | ||
"version": "1.2.11", | ||
"version": "1.3.0", | ||
"main": "src/scribe.js", | ||
@@ -5,0 +5,0 @@ "dependencies": { |
@@ -10,12 +10,7 @@ define(function () { | ||
redoCommand.execute = function () { | ||
var historyItem = scribe.undoManager.redo(); | ||
if (typeof historyItem !== 'undefined') { | ||
scribe.restoreFromHistory(historyItem); | ||
} | ||
scribe.undoManager.redo(); | ||
}; | ||
redoCommand.queryEnabled = function () { | ||
return scribe.undoManager.position < scribe.undoManager.stack.length - 1; | ||
return scribe.undoManager.position > 0; | ||
}; | ||
@@ -22,0 +17,0 @@ |
@@ -10,11 +10,7 @@ define(function () { | ||
undoCommand.execute = function () { | ||
var historyItem = scribe.undoManager.undo(); | ||
if (typeof historyItem !== 'undefined') { | ||
scribe.restoreFromHistory(historyItem); | ||
} | ||
scribe.undoManager.undo(); | ||
}; | ||
undoCommand.queryEnabled = function () { | ||
return scribe.undoManager.position > 1; | ||
return scribe.undoManager.position < scribe.undoManager.length; | ||
}; | ||
@@ -21,0 +17,0 @@ |
@@ -14,16 +14,2 @@ define([ | ||
/** | ||
* Push the first history item when the editor is focused. | ||
*/ | ||
var pushHistoryOnFocus = function () { | ||
// Tabbing into the editor doesn't create a range immediately, so we | ||
// have to wait until the next event loop. | ||
setTimeout(function () { | ||
scribe.pushHistory(); | ||
}.bind(scribe), 0); | ||
scribe.el.removeEventListener('focus', pushHistoryOnFocus); | ||
}.bind(scribe); | ||
scribe.el.addEventListener('focus', pushHistoryOnFocus); | ||
/** | ||
* Firefox: Giving focus to a `contenteditable` will place the caret | ||
@@ -95,15 +81,10 @@ * outside of any block elements. Chrome behaves correctly by placing the | ||
// happens on the first `focus` event). | ||
if (isEditorActive) { | ||
if (scribe.undoManager) { | ||
// Discard the last history item, as we're going to be adding | ||
// a new clean history item next. | ||
scribe.undoManager.undo(); | ||
} | ||
// Pass content through formatters, place caret back | ||
scribe.transactionManager.run(runFormatters); | ||
} else { | ||
runFormatters(); | ||
} | ||
// The previous check is no longer needed, and the above comments are no longer valid. | ||
// Now `scribe.setContent` updates the content manually, and `scribe.pushHistory` | ||
// will not detect any changes, and nothing will be push into the history. | ||
// Any mutations made without `scribe.getContent` will be pushed into the history normally. | ||
// Pass content through formatters, place caret back | ||
scribe.transactionManager.run(runFormatters); | ||
} | ||
@@ -110,0 +91,0 @@ |
@@ -41,5 +41,5 @@ define([], function () { | ||
* and recorded the faulty content as an item in the | ||
* UndoManager. We interfere with the undoManager | ||
* here to discard that history item, and let the next | ||
* transaction run produce a clean one instead. | ||
* UndoManager. We interfere with the undoManager | ||
* by force merging that transaction with the next | ||
* transaction which produce a clean one instead. | ||
* | ||
@@ -51,5 +51,2 @@ * FIXME: ideally we would not trigger a | ||
*/ | ||
if (scribe.undoManager) { | ||
scribe.undoManager.undo(); | ||
} | ||
@@ -86,3 +83,3 @@ scribe.transactionManager.run(function () { | ||
selection.selectMarkers(); | ||
}); | ||
}, true); | ||
} | ||
@@ -89,0 +86,0 @@ } |
@@ -32,3 +32,3 @@ define([ | ||
buildTransactionManager, | ||
buildUndoManager, | ||
UndoManager, | ||
EventEmitter, | ||
@@ -51,3 +51,8 @@ elementHelpers, | ||
debug: false, | ||
undo: { enabled: true }, | ||
undo: { | ||
manager: false, | ||
enabled: true, | ||
limit: 100, | ||
interval: 250 | ||
}, | ||
defaultCommandPatches: [ | ||
@@ -80,6 +85,16 @@ 'bold', | ||
if (this.options.undo.enabled) { | ||
var UndoManager = buildUndoManager(this); | ||
this.undoManager = new UndoManager(); | ||
if (this.options.undo.manager) { | ||
this.undoManager = this.options.undo.manager; | ||
} | ||
else { | ||
this.undoManager = new UndoManager(this.options.undo.limit, this.el); | ||
} | ||
this._merge = false; | ||
this._forceMerge = false; | ||
this._mergeTimer = 0; | ||
this._lastItem = {content: ''}; | ||
} | ||
this.setHTML(this.getHTML()); | ||
this.el.setAttribute('contenteditable', true); | ||
@@ -163,2 +178,4 @@ | ||
Scribe.prototype.setHTML = function (html, skipFormatters) { | ||
this._lastItem.content = html; | ||
if (skipFormatters) { | ||
@@ -187,31 +204,55 @@ this._skipFormatters = true; | ||
Scribe.prototype.pushHistory = function () { | ||
if (this.options.undo.enabled) { | ||
var previousUndoItem = this.undoManager.stack[this.undoManager.position]; | ||
var previousContent = previousUndoItem && previousUndoItem | ||
/** | ||
* Chrome and Firefox: If we did push to the history, this would break | ||
* browser magic around `Document.queryCommandState` (http://jsbin.com/eDOxacI/1/edit?js,console,output). | ||
* This happens when doing any DOM manipulation. | ||
*/ | ||
var scribe = this; | ||
if (scribe.options.undo.enabled) { | ||
// Get scribe previous content, and strip markers. | ||
var lastContentNoMarkers = scribe._lastItem.content | ||
.replace(/<em class="scribe-marker">/g, '').replace(/<\/em>/g, ''); | ||
/** | ||
* Chrome and Firefox: If we did push to the history, this would break | ||
* browser magic around `Document.queryCommandState` (http://jsbin.com/eDOxacI/1/edit?js,console,output). | ||
* This happens when doing any DOM manipulation. | ||
*/ | ||
// We only want to push the history if the content actually changed. | ||
if (! previousUndoItem || (previousUndoItem && this.getHTML() !== previousContent)) { | ||
var selection = new this.api.Selection(); | ||
if (scribe.getHTML() !== lastContentNoMarkers) { | ||
var selection = new scribe.api.Selection(); | ||
selection.placeMarkers(); | ||
var html = this.getHTML(); | ||
var content = scribe.getHTML(); | ||
selection.removeMarkers(); | ||
this.undoManager.push(html); | ||
// Checking if there is a need to merge, and that the previous history item | ||
// is the last history item of the same scribe instance. | ||
// It is possible the last transaction is not for the same instance, or | ||
// even not a scribe transaction (e.g. when using a shared undo manager). | ||
var previousItem = scribe.undoManager.item(scribe.undoManager.position); | ||
if ((scribe._merge || scribe._forceMerge) && previousItem && scribe._lastItem == previousItem[0]) { | ||
// If so, merge manually with the last item to save more memory space. | ||
scribe._lastItem.content = content; | ||
} | ||
else { | ||
// Otherwise, create a new history item, and register it as a new transaction | ||
scribe._lastItem = { | ||
previousItem: scribe._lastItem, | ||
content: content, | ||
scribe: scribe, | ||
execute: function () { }, | ||
undo: function () { this.scribe.restoreFromHistory(this.previousItem); }, | ||
redo: function () { this.scribe.restoreFromHistory(this); } | ||
}; | ||
scribe.undoManager.transact(scribe._lastItem, false); | ||
} | ||
// Merge next transaction if it happens before the interval option, otherwise don't merge. | ||
clearTimeout(scribe._mergeTimer); | ||
scribe._merge = true; | ||
scribe._mergeTimer = setTimeout(function() { scribe._merge = false; }, scribe.options.undo.interval); | ||
return true; | ||
} else { | ||
return false; | ||
} | ||
} else { | ||
return false; | ||
} | ||
return false; | ||
}; | ||
@@ -224,4 +265,6 @@ | ||
Scribe.prototype.restoreFromHistory = function (historyItem) { | ||
this.setHTML(historyItem, true); | ||
this._lastItem = historyItem; | ||
this.setHTML(historyItem.content, true); | ||
// Restore the selection | ||
@@ -228,0 +271,0 @@ var selection = new this.api.Selection(); |
@@ -24,3 +24,3 @@ define(['lodash-amd/modern/objects/assign'], function (assign) { | ||
run: function (transaction) { | ||
run: function (transaction, forceMerge) { | ||
this.start(); | ||
@@ -33,3 +33,5 @@ // If there is an error, don't prevent the transaction from ending. | ||
} finally { | ||
scribe._forceMerge = forceMerge === true; | ||
this.end(); | ||
scribe._forceMerge = false; | ||
} | ||
@@ -36,0 +38,0 @@ } |
define(function () { | ||
'use strict'; | ||
return function (scribe) { | ||
function UndoManager(limit, undoScopeHost) { | ||
this._stack = []; | ||
this._limit = limit; | ||
this._fireEvent = typeof CustomEvent != 'undefined' && undoScopeHost && undoScopeHost.dispatchEvent; | ||
this._ush = undoScopeHost; | ||
function UndoManager() { | ||
this.position = -1; | ||
this.stack = []; | ||
this.debug = scribe.isDebugModeEnabled(); | ||
this.position = 0; | ||
this.length = 0; | ||
} | ||
UndoManager.prototype.transact = function (transaction, merge) { | ||
if (arguments.length < 2) { | ||
throw new TypeError('Not enough arguments to UndoManager.transact.'); | ||
} | ||
UndoManager.prototype.maxStackSize = 100; | ||
transaction.execute(); | ||
UndoManager.prototype.push = function (item) { | ||
if (this.debug) { | ||
console.log('UndoManager.push: %s', item); | ||
this._stack.splice(0, this.position); | ||
if (merge && this.length) { | ||
this._stack[0].push(transaction); | ||
} | ||
else { | ||
this._stack.unshift([transaction]); | ||
} | ||
this.position = 0; | ||
if (this._limit && this._stack.length > this._limit) { | ||
this.length = this._stack.length = this._limit; | ||
} | ||
else { | ||
this.length = this._stack.length; | ||
} | ||
if (this._fireEvent) { | ||
this._ush.dispatchEvent(new CustomEvent('DOMTransaction', {detail: {transactions: this._stack[0].slice()}, bubbles: true, cancelable: false})); | ||
} | ||
}; | ||
UndoManager.prototype.undo = function () { | ||
if (this.position < this.length) { | ||
for (var i = this._stack[this.position].length - 1; i >= 0; i--) { | ||
this._stack[this.position][i].undo(); | ||
} | ||
this.stack.length = ++this.position; | ||
this.stack.push(item); | ||
this.position++; | ||
while (this.stack.length > this.maxStackSize) { | ||
this.stack.shift(); | ||
--this.position; | ||
if (this._fireEvent) { | ||
this._ush.dispatchEvent(new CustomEvent('undo', {detail: {transactions: this._stack[this.position - 1].slice()}, bubbles: true, cancelable: false})); | ||
} | ||
}; | ||
} | ||
}; | ||
UndoManager.prototype.undo = function () { | ||
if (this.position > 0) { | ||
return this.stack[--this.position]; | ||
UndoManager.prototype.redo = function () { | ||
if (this.position > 0) { | ||
for (var i = 0, n = this._stack[this.position - 1].length; i < n; i++) { | ||
this._stack[this.position - 1][i].redo(); | ||
} | ||
}; | ||
this.position--; | ||
UndoManager.prototype.redo = function () { | ||
if (this.position < (this.stack.length - 1)) { | ||
return this.stack[++this.position]; | ||
if (this._fireEvent) { | ||
this._ush.dispatchEvent(new CustomEvent('redo', {detail: {transactions: this._stack[this.position].slice()}, bubbles: true, cancelable: false})); | ||
} | ||
}; | ||
} | ||
}; | ||
return UndoManager; | ||
UndoManager.prototype.item = function (index) { | ||
if (index >= 0 && index < this.length) { | ||
return this._stack[index].slice(); | ||
} | ||
return null; | ||
}; | ||
UndoManager.prototype.clearUndo = function () { | ||
this._stack.length = this.length = this.position; | ||
}; | ||
UndoManager.prototype.clearRedo = function () { | ||
this._stack.splice(0, this.position); | ||
this.position = 0; | ||
this.length = this._stack.length; | ||
}; | ||
return UndoManager; | ||
}); | ||
@@ -27,2 +27,10 @@ var chai = require('chai'); | ||
}); | ||
// Undo manager merge interval set to 0ms (default is 1000ms). | ||
// This will avoid merging instant typing transactions as performed by these automated tests. | ||
beforeEach(function () { | ||
return driver.executeScript(function () { | ||
window.scribe.options.undo.interval = 0; | ||
}); | ||
}); | ||
@@ -29,0 +37,0 @@ givenContentOf('<p>|1</p>', function () { |
Sorry, the diff of this file is not supported yet
191163
70
3934