| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- /**
- * UndoManager.js
- *
- * Copyright, Moxiecode Systems AB
- * Released under LGPL License.
- *
- * License: http://www.tinymce.com/license
- * Contributing: http://www.tinymce.com/contributing
- */
- /**
- * This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed.
- *
- * @class tinymce.UndoManager
- */
- define("tinymce/UndoManager", [
- "tinymce/Env",
- "tinymce/util/Tools"
- ], function(Env, Tools) {
- var trim = Tools.trim, trimContentRegExp;
- trimContentRegExp = new RegExp([
- '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
- '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles
- '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
- ].join('|'), 'gi');
- return function(editor) {
- var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
- // Returns a trimmed version of the current editor contents
- function getContent() {
- return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, ''));
- }
- function addNonTypingUndoLevel(e) {
- self.typing = false;
- self.add({}, e);
- }
- // Add initial undo level when the editor is initialized
- editor.on('init', function() {
- self.add();
- });
- // Get position before an execCommand is processed
- editor.on('BeforeExecCommand', function(e) {
- var cmd = e.command;
- if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
- self.beforeChange();
- }
- });
- // Add undo level after an execCommand call was made
- editor.on('ExecCommand', function(e) {
- var cmd = e.command;
- if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
- addNonTypingUndoLevel(e);
- }
- });
- editor.on('ObjectResizeStart', function() {
- self.beforeChange();
- });
- editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
- editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
- editor.on('KeyUp', function(e) {
- var keyCode = e.keyCode;
- if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
- addNonTypingUndoLevel();
- editor.nodeChanged();
- }
- if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) {
- editor.nodeChanged();
- }
- // Fire a TypingUndo event on the first character entered
- if (isFirstTypedCharacter && self.typing) {
- // Make the it dirty if the content was changed after typing the first character
- if (!editor.isDirty()) {
- editor.isNotDirty = !data[0] || getContent() == data[0].content;
- // Fire initial change event
- if (!editor.isNotDirty) {
- editor.fire('change', {level: data[0], lastLevel: null});
- }
- }
- editor.fire('TypingUndo');
- isFirstTypedCharacter = false;
- editor.nodeChanged();
- }
- });
- editor.on('KeyDown', function(e) {
- var keyCode = e.keyCode;
- // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
- if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
- if (self.typing) {
- addNonTypingUndoLevel(e);
- }
- return;
- }
- // If key isn't shift,ctrl,alt,capslock,metakey
- if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
- self.beforeChange();
- self.typing = true;
- self.add({}, e);
- isFirstTypedCharacter = true;
- }
- });
- editor.on('MouseDown', function(e) {
- if (self.typing) {
- addNonTypingUndoLevel(e);
- }
- });
- // Add keyboard shortcuts for undo/redo keys
- editor.addShortcut('ctrl+z', '', 'Undo');
- editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo');
- editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) {
- if (!e.isDefaultPrevented()) {
- editor.nodeChanged();
- }
- });
- self = {
- // Explose for debugging reasons
- data: data,
- /**
- * State if the user is currently typing or not. This will add a typing operation into one undo
- * level instead of one new level for each keystroke.
- *
- * @field {Boolean} typing
- */
- typing: false,
- /**
- * Stores away a bookmark to be used when performing an undo action so that the selection is before
- * the change has been made.
- *
- * @method beforeChange
- */
- beforeChange: function() {
- if (!locks) {
- beforeBookmark = editor.selection.getBookmark(2, true);
- }
- },
- /**
- * Adds a new undo level/snapshot to the undo list.
- *
- * @method add
- * @param {Object} level Optional undo level object to add.
- * @param {DOMEvent} Event Optional event responsible for the creation of the undo level.
- * @return {Object} Undo level that got added or null it a level wasn't needed.
- */
- add: function(level, event) {
- var i, settings = editor.settings, lastLevel;
- level = level || {};
- level.content = getContent();
- if (locks || editor.removed) {
- return null;
- }
- lastLevel = data[index];
- if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
- return null;
- }
- // Add undo level if needed
- if (lastLevel && lastLevel.content == level.content) {
- return null;
- }
- // Set before bookmark on previous level
- if (data[index]) {
- data[index].beforeBookmark = beforeBookmark;
- }
- // Time to compress
- if (settings.custom_undo_redo_levels) {
- if (data.length > settings.custom_undo_redo_levels) {
- for (i = 0; i < data.length - 1; i++) {
- data[i] = data[i + 1];
- }
- data.length--;
- index = data.length;
- }
- }
- // Get a non intrusive normalized bookmark
- level.bookmark = editor.selection.getBookmark(2, true);
- // Crop array if needed
- if (index < data.length - 1) {
- data.length = index + 1;
- }
- data.push(level);
- index = data.length - 1;
- var args = {level: level, lastLevel: lastLevel, originalEvent: event};
- editor.fire('AddUndo', args);
- if (index > 0) {
- editor.isNotDirty = false;
- editor.fire('change', args);
- }
- return level;
- },
- /**
- * Undoes the last action.
- *
- * @method undo
- * @return {Object} Undo level or null if no undo was performed.
- */
- undo: function() {
- var level;
- if (self.typing) {
- self.add();
- self.typing = false;
- }
- if (index > 0) {
- level = data[--index];
- // Undo to first index then set dirty state to false
- if (index === 0) {
- editor.isNotDirty = true;
- }
- editor.setContent(level.content, {format: 'raw'});
- editor.selection.moveToBookmark(level.beforeBookmark);
- editor.fire('undo', {level: level});
- }
- return level;
- },
- /**
- * Redoes the last action.
- *
- * @method redo
- * @return {Object} Redo level or null if no redo was performed.
- */
- redo: function() {
- var level;
- if (index < data.length - 1) {
- level = data[++index];
- editor.setContent(level.content, {format: 'raw'});
- editor.selection.moveToBookmark(level.bookmark);
- editor.fire('redo', {level: level});
- }
- return level;
- },
- /**
- * Removes all undo levels.
- *
- * @method clear
- */
- clear: function() {
- data = [];
- index = 0;
- self.typing = false;
- editor.fire('ClearUndos');
- },
- /**
- * Returns true/false if the undo manager has any undo levels.
- *
- * @method hasUndo
- * @return {Boolean} true/false if the undo manager has any undo levels.
- */
- hasUndo: function() {
- // Has undo levels or typing and content isn't the same as the initial level
- return index > 0 || (self.typing && data[0] && getContent() != data[0].content);
- },
- /**
- * Returns true/false if the undo manager has any redo levels.
- *
- * @method hasRedo
- * @return {Boolean} true/false if the undo manager has any redo levels.
- */
- hasRedo: function() {
- return index < data.length - 1 && !this.typing;
- },
- /**
- * Executes the specified function in an undo transation. The selection
- * before the modification will be stored to the undo stack and if the DOM changes
- * it will add a new undo level. Any methods within the transation that adds undo levels will
- * be ignored. So a transation can include calls to execCommand or editor.insertContent.
- *
- * @method transact
- * @param {function} callback Function to execute dom manipulation logic in.
- */
- transact: function(callback) {
- self.beforeChange();
- try {
- locks++;
- callback();
- } finally {
- locks--;
- }
- self.add();
- }
- };
- return self;
- };
- });
|