12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328 |
- /**
- * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md or http://ckeditor.com/license
- */
- /**
- * @fileOverview Undo/Redo system for saving a shapshot for document modification
- * and other recordable changes.
- */
- 'use strict';
- ( function() {
- var keystrokes = [
- CKEDITOR.CTRL + 90 /*Z*/,
- CKEDITOR.CTRL + 89 /*Y*/,
- CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/
- ],
- backspaceOrDelete = { 8: 1, 46: 1 };
- CKEDITOR.plugins.add( 'undo', {
- // jscs:disable maximumLineLength
- lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%
- // jscs:enable maximumLineLength
- icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%
- hidpi: true, // %REMOVE_LINE_CORE%
- init: function( editor ) {
- var undoManager = editor.undoManager = new UndoManager( editor ),
- editingHandler = undoManager.editingHandler = new NativeEditingHandler( undoManager );
- var undoCommand = editor.addCommand( 'undo', {
- exec: function() {
- if ( undoManager.undo() ) {
- editor.selectionChange();
- this.fire( 'afterUndo' );
- }
- },
- startDisabled: true,
- canUndo: false
- } );
- var redoCommand = editor.addCommand( 'redo', {
- exec: function() {
- if ( undoManager.redo() ) {
- editor.selectionChange();
- this.fire( 'afterRedo' );
- }
- },
- startDisabled: true,
- canUndo: false
- } );
- editor.setKeystroke( [
- [ keystrokes[ 0 ], 'undo' ],
- [ keystrokes[ 1 ], 'redo' ],
- [ keystrokes[ 2 ], 'redo' ]
- ] );
- undoManager.onChange = function() {
- undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
- redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
- };
- function recordCommand( event ) {
- // If the command hasn't been marked to not support undo.
- if ( undoManager.enabled && event.data.command.canUndo !== false )
- undoManager.save();
- }
- // We'll save snapshots before and after executing a command.
- editor.on( 'beforeCommandExec', recordCommand );
- editor.on( 'afterCommandExec', recordCommand );
- // Save snapshots before doing custom changes.
- editor.on( 'saveSnapshot', function( evt ) {
- undoManager.save( evt.data && evt.data.contentOnly );
- } );
- // Event manager listeners should be attached on contentDom.
- editor.on( 'contentDom', editingHandler.attachListeners, editingHandler );
- editor.on( 'instanceReady', function() {
- // Saves initial snapshot.
- editor.fire( 'saveSnapshot' );
- } );
- // Always save an undo snapshot - the previous mode might have
- // changed editor contents.
- editor.on( 'beforeModeUnload', function() {
- editor.mode == 'wysiwyg' && undoManager.save( true );
- } );
- function toggleUndoManager() {
- undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
- undoManager.onChange();
- }
- // Make the undo manager available only in wysiwyg mode.
- editor.on( 'mode', toggleUndoManager );
- // Disable undo manager when in read-only mode.
- editor.on( 'readOnly', toggleUndoManager );
- if ( editor.ui.addButton ) {
- editor.ui.addButton( 'Undo', {
- label: editor.lang.undo.undo,
- command: 'undo',
- toolbar: 'undo,10'
- } );
- editor.ui.addButton( 'Redo', {
- label: editor.lang.undo.redo,
- command: 'redo',
- toolbar: 'undo,20'
- } );
- }
- /**
- * Resets the undo stack.
- *
- * @member CKEDITOR.editor
- */
- editor.resetUndo = function() {
- // Reset the undo stack.
- undoManager.reset();
- // Create the first image.
- editor.fire( 'saveSnapshot' );
- };
- /**
- * Amends the top of the undo stack (last undo image) with the current DOM changes.
- *
- * function() {
- * editor.fire( 'saveSnapshot' );
- * editor.document.body.append(...);
- * // Makes new changes following the last undo snapshot a part of it.
- * editor.fire( 'updateSnapshot' );
- * ..
- * }
- *
- * @event updateSnapshot
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- */
- editor.on( 'updateSnapshot', function() {
- if ( undoManager.currentImage )
- undoManager.update();
- } );
- /**
- * Locks the undo manager to prevent any save/update operations.
- *
- * It is convenient to lock the undo manager before performing DOM operations
- * that should not be recored (e.g. auto paragraphing).
- *
- * See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.
- *
- * **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired
- * the same number of times that `lockSnapshot` has been fired.
- *
- * @since 4.0
- * @event lockSnapshot
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- * @param data
- * @param {Boolean} [data.dontUpdate] When set to `true`, the last snapshot will not be updated
- * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
- * @param {Boolean} [data.forceUpdate] When set to `true`, the last snapshot will always be updated
- * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
- */
- editor.on( 'lockSnapshot', function( evt ) {
- var data = evt.data;
- undoManager.lock( data && data.dontUpdate, data && data.forceUpdate );
- } );
- /**
- * Unlocks the undo manager and updates the latest snapshot.
- *
- * @since 4.0
- * @event unlockSnapshot
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- */
- editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );
- }
- } );
- CKEDITOR.plugins.undo = {};
- /**
- * Main logic for the Redo/Undo feature.
- *
- * @private
- * @class CKEDITOR.plugins.undo.UndoManager
- * @constructor Creates an UndoManager class instance.
- * @param {CKEDITOR.editor} editor
- */
- var UndoManager = CKEDITOR.plugins.undo.UndoManager = function( editor ) {
- /**
- * An array storing the number of key presses, count in a row. Use {@link #keyGroups} members as index.
- *
- * **Note:** The keystroke count will be reset after reaching the limit of characters per snapshot.
- *
- * @since 4.4.4
- */
- this.strokesRecorded = [ 0, 0 ];
- /**
- * When the `locked` property is not `null`, the undo manager is locked, so
- * operations like `save` or `update` are forbidden.
- *
- * The manager can be locked and unlocked by the {@link #lock} and {@link #unlock}
- * methods, respectively.
- *
- * @readonly
- * @property {Object} [locked=null]
- */
- this.locked = null;
- /**
- * Contains the previously processed key group, based on {@link #keyGroups}.
- * `-1` means an unknown group.
- *
- * @since 4.4.4
- * @readonly
- * @property {Number} [previousKeyGroup=-1]
- */
- this.previousKeyGroup = -1;
- /**
- * The maximum number of snapshots in the stack. Configurable via {@link CKEDITOR.config#undoStackSize}.
- *
- * @readonly
- * @property {Number} [limit]
- */
- this.limit = editor.config.undoStackSize || 20;
- /**
- * The maximum number of characters typed/deleted in one undo step.
- *
- * @since 4.4.5
- * @readonly
- */
- this.strokesLimit = 25;
- this.editor = editor;
- // Reset the undo stack.
- this.reset();
- };
- UndoManager.prototype = {
- /**
- * Handles keystroke support for the undo manager. It is called on `keyup` event for
- * keystrokes that can change the editor content.
- *
- * @param {Number} keyCode The key code.
- * @param {Boolean} [strokesPerSnapshotExceeded] When set to `true`, the method will
- * behave as if the strokes limit was exceeded regardless of the {@link #strokesRecorded} value.
- */
- type: function( keyCode, strokesPerSnapshotExceeded ) {
- var keyGroup = UndoManager.getKeyGroup( keyCode ),
- // Count of keystrokes in current a row.
- // Note if strokesPerSnapshotExceeded will be exceeded, it'll be restarted.
- strokesRecorded = this.strokesRecorded[ keyGroup ] + 1;
- strokesPerSnapshotExceeded =
- ( strokesPerSnapshotExceeded || strokesRecorded >= this.strokesLimit );
- if ( !this.typing )
- onTypingStart( this );
- if ( strokesPerSnapshotExceeded ) {
- // Reset the count of strokes, so it'll be later assigned to this.strokesRecorded.
- strokesRecorded = 0;
- this.editor.fire( 'saveSnapshot' );
- } else {
- // Fire change event.
- this.editor.fire( 'change' );
- }
- // Store recorded strokes count.
- this.strokesRecorded[ keyGroup ] = strokesRecorded;
- // This prop will tell in next itaration what kind of group was processed previously.
- this.previousKeyGroup = keyGroup;
- },
- /**
- * Whether the new `keyCode` belongs to a different group than the previous one ({@link #previousKeyGroup}).
- *
- * @since 4.4.5
- * @param {Number} keyCode
- * @returns {Boolean}
- */
- keyGroupChanged: function( keyCode ) {
- return UndoManager.getKeyGroup( keyCode ) != this.previousKeyGroup;
- },
- /**
- * Resets the undo stack.
- */
- reset: function() {
- // Stack for all the undo and redo snapshots, they're always created/removed
- // in consistency.
- this.snapshots = [];
- // Current snapshot history index.
- this.index = -1;
- this.currentImage = null;
- this.hasUndo = false;
- this.hasRedo = false;
- this.locked = null;
- this.resetType();
- },
- /**
- * Resets all typing variables.
- *
- * @see #type
- */
- resetType: function() {
- this.strokesRecorded = [ 0, 0 ];
- this.typing = false;
- this.previousKeyGroup = -1;
- },
- /**
- * Refreshes the state of the {@link CKEDITOR.plugins.undo.UndoManager undo manager}
- * as well as the state of the `undo` and `redo` commands.
- */
- refreshState: function() {
- // These lines can be handled within onChange() too.
- this.hasUndo = !!this.getNextImage( true );
- this.hasRedo = !!this.getNextImage( false );
- // Reset typing
- this.resetType();
- this.onChange();
- },
- /**
- * Saves a snapshot of the document image for later retrieval.
- *
- * @param {Boolean} onContentOnly If set to `true`, the snapshot will be saved only if the content has changed.
- * @param {CKEDITOR.plugins.undo.Image} image An optional image to save. If skipped, current editor will be used.
- * @param {Boolean} [autoFireChange=true] If set to `false`, will not trigger the {@link CKEDITOR.editor#change} event to editor.
- */
- save: function( onContentOnly, image, autoFireChange ) {
- var editor = this.editor;
- // Do not change snapshots stack when locked, editor is not ready,
- // editable is not ready or when editor is in mode difference than 'wysiwyg'.
- if ( this.locked || editor.status != 'ready' || editor.mode != 'wysiwyg' )
- return false;
- var editable = editor.editable();
- if ( !editable || editable.status != 'ready' )
- return false;
- var snapshots = this.snapshots;
- // Get a content image.
- if ( !image )
- image = new Image( editor );
- // Do nothing if it was not possible to retrieve an image.
- if ( image.contents === false )
- return false;
- // Check if this is a duplicate. In such case, do nothing.
- if ( this.currentImage ) {
- if ( image.equalsContent( this.currentImage ) ) {
- if ( onContentOnly )
- return false;
- if ( image.equalsSelection( this.currentImage ) )
- return false;
- } else if ( autoFireChange !== false ) {
- editor.fire( 'change' );
- }
- }
- // Drop future snapshots.
- snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
- // If we have reached the limit, remove the oldest one.
- if ( snapshots.length == this.limit )
- snapshots.shift();
- // Add the new image, updating the current index.
- this.index = snapshots.push( image ) - 1;
- this.currentImage = image;
- if ( autoFireChange !== false )
- this.refreshState();
- return true;
- },
- /**
- * Sets editor content/selection to the one stored in `image`.
- *
- * @param {CKEDITOR.plugins.undo.Image} image
- */
- restoreImage: function( image ) {
- // Bring editor focused to restore selection.
- var editor = this.editor,
- sel;
- if ( image.bookmarks ) {
- editor.focus();
- // Retrieve the selection beforehand. (#8324)
- sel = editor.getSelection();
- }
- // Start transaction - do not allow any mutations to the
- // snapshots stack done when selecting bookmarks (much probably
- // by selectionChange listener).
- this.locked = { level: 999 };
- this.editor.loadSnapshot( image.contents );
- if ( image.bookmarks )
- sel.selectBookmarks( image.bookmarks );
- else if ( CKEDITOR.env.ie ) {
- // IE BUG: If I don't set the selection to *somewhere* after setting
- // document contents, then IE would create an empty paragraph at the bottom
- // the next time the document is modified.
- var $range = this.editor.document.getBody().$.createTextRange();
- $range.collapse( true );
- $range.select();
- }
- this.locked = null;
- this.index = image.index;
- this.currentImage = this.snapshots[ this.index ];
- // Update current image with the actual editor
- // content, since actualy content may differ from
- // the original snapshot due to dom change. (#4622)
- this.update();
- this.refreshState();
- editor.fire( 'change' );
- },
- /**
- * Gets the closest available image.
- *
- * @param {Boolean} isUndo If `true`, it will return the previous image.
- * @returns {CKEDITOR.plugins.undo.Image} Next image or `null`.
- */
- getNextImage: function( isUndo ) {
- var snapshots = this.snapshots,
- currentImage = this.currentImage,
- image, i;
- if ( currentImage ) {
- if ( isUndo ) {
- for ( i = this.index - 1; i >= 0; i-- ) {
- image = snapshots[ i ];
- if ( !currentImage.equalsContent( image ) ) {
- image.index = i;
- return image;
- }
- }
- } else {
- for ( i = this.index + 1; i < snapshots.length; i++ ) {
- image = snapshots[ i ];
- if ( !currentImage.equalsContent( image ) ) {
- image.index = i;
- return image;
- }
- }
- }
- }
- return null;
- },
- /**
- * Checks the current redo state.
- *
- * @returns {Boolean} Whether the document has a previous state to retrieve.
- */
- redoable: function() {
- return this.enabled && this.hasRedo;
- },
- /**
- * Checks the current undo state.
- *
- * @returns {Boolean} Whether the document has a future state to restore.
- */
- undoable: function() {
- return this.enabled && this.hasUndo;
- },
- /**
- * Performs an undo operation on current index.
- */
- undo: function() {
- if ( this.undoable() ) {
- this.save( true );
- var image = this.getNextImage( true );
- if ( image )
- return this.restoreImage( image ), true;
- }
- return false;
- },
- /**
- * Performs a redo operation on current index.
- */
- redo: function() {
- if ( this.redoable() ) {
- // Try to save. If no changes have been made, the redo stack
- // will not change, so it will still be redoable.
- this.save( true );
- // If instead we had changes, we can't redo anymore.
- if ( this.redoable() ) {
- var image = this.getNextImage( false );
- if ( image )
- return this.restoreImage( image ), true;
- }
- }
- return false;
- },
- /**
- * Updates the last snapshot of the undo stack with the current editor content.
- *
- * @param {CKEDITOR.plugins.undo.Image} [newImage] The image which will replace the current one.
- * If it is not set, it defaults to the image taken from the editor.
- */
- update: function( newImage ) {
- // Do not change snapshots stack is locked.
- if ( this.locked )
- return;
- if ( !newImage )
- newImage = new Image( this.editor );
- var i = this.index,
- snapshots = this.snapshots;
- // Find all previous snapshots made for the same content (which differ
- // only by selection) and replace all of them with the current image.
- while ( i > 0 && this.currentImage.equalsContent( snapshots[ i - 1 ] ) )
- i -= 1;
- snapshots.splice( i, this.index - i + 1, newImage );
- this.index = i;
- this.currentImage = newImage;
- },
- /**
- * Amends the last snapshot and changes its selection (only in case when content
- * is equal between these two).
- *
- * @since 4.4.4
- * @param {CKEDITOR.plugins.undo.Image} newSnapshot New snapshot with new selection.
- * @returns {Boolean} Returns `true` if selection was amended.
- */
- updateSelection: function( newSnapshot ) {
- if ( !this.snapshots.length )
- return false;
- var snapshots = this.snapshots,
- lastImage = snapshots[ snapshots.length - 1 ];
- if ( lastImage.equalsContent( newSnapshot ) ) {
- if ( !lastImage.equalsSelection( newSnapshot ) ) {
- snapshots[ snapshots.length - 1 ] = newSnapshot;
- this.currentImage = newSnapshot;
- return true;
- }
- }
- return false;
- },
- /**
- * Locks the snapshot stack to prevent any save/update operations and when necessary,
- * updates the tip of the snapshot stack with the DOM changes introduced during the
- * locked period, after the {@link #unlock} method is called.
- *
- * It is mainly used to ensure any DOM operations that should not be recorded
- * (e.g. auto paragraphing) are not added to the stack.
- *
- * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.
- *
- * @since 4.0
- * @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated
- * with current content and selection. By default, if undo manager was up to date when the lock started,
- * the last snapshot will be updated to the current state when unlocking. This means that all changes
- * done during the lock will be merged into the previous snapshot or the next one. Use this option to gain
- * more control over this behavior. For example, it is possible to group changes done during the lock into
- * a separate snapshot.
- * @param {Boolean} [forceUpdate] When set to `true`, the last snapshot will always be updated with the
- * current content and selection regardless of the current state of the undo manager.
- * When not set, the last snapshot will be updated only if the undo manager was up to date when locking.
- * Additionally, this option makes it possible to lock the snapshot when the editor is not in the `wysiwyg` mode,
- * because when it is passed, the snapshots will not need to be compared.
- */
- lock: function( dontUpdate, forceUpdate ) {
- if ( !this.locked ) {
- if ( dontUpdate )
- this.locked = { level: 1 };
- else {
- var update = null;
- if ( forceUpdate )
- update = true;
- else {
- // Make a contents image. Don't include bookmarks, because:
- // * we don't compare them,
- // * there's a chance that DOM has been changed since
- // locked (e.g. fake) selection was made, so createBookmark2 could fail.
- // http://dev.ckeditor.com/ticket/11027#comment:3
- var imageBefore = new Image( this.editor, true );
- // If current editor content matches the tip of snapshot stack,
- // the stack tip must be updated by unlock, to include any changes made
- // during this period.
- if ( this.currentImage && this.currentImage.equalsContent( imageBefore ) )
- update = imageBefore;
- }
- this.locked = { update: update, level: 1 };
- }
- // Increase the level of lock.
- } else {
- this.locked.level++;
- }
- },
- /**
- * Unlocks the snapshot stack and checks to amend the last snapshot.
- *
- * See {@link #lock} for more details.
- *
- * @since 4.0
- */
- unlock: function() {
- if ( this.locked ) {
- // Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
- if ( !--this.locked.level ) {
- var update = this.locked.update;
- this.locked = null;
- // forceUpdate was passed to lock().
- if ( update === true )
- this.update();
- // update is instance of Image.
- else if ( update ) {
- var newImage = new Image( this.editor, true );
- if ( !update.equalsContent( newImage ) )
- this.update();
- }
- }
- }
- }
- };
- /**
- * Codes for navigation keys like *Arrows*, *Page Up/Down*, etc.
- * Used by the {@link #isNavigationKey} method.
- *
- * @since 4.4.5
- * @readonly
- * @static
- */
- UndoManager.navigationKeyCodes = {
- 37: 1, 38: 1, 39: 1, 40: 1, // Arrows.
- 36: 1, 35: 1, // Home, End.
- 33: 1, 34: 1 // PgUp, PgDn.
- };
- /**
- * Key groups identifier mapping. Used for accessing members in
- * {@link #strokesRecorded}.
- *
- * * `FUNCTIONAL` – identifier for the *Backspace* / *Delete* key.
- * * `PRINTABLE` – identifier for printable keys.
- *
- * Example usage:
- *
- * undoManager.strokesRecorded[ undoManager.keyGroups.FUNCTIONAL ];
- *
- * @since 4.4.5
- * @readonly
- * @static
- */
- UndoManager.keyGroups = {
- PRINTABLE: 0,
- FUNCTIONAL: 1
- };
- /**
- * Checks whether a key is one of navigation keys (*Arrows*, *Page Up/Down*, etc.).
- * See also the {@link #navigationKeyCodes} property.
- *
- * @since 4.4.5
- * @static
- * @param {Number} keyCode
- * @returns {Boolean}
- */
- UndoManager.isNavigationKey = function( keyCode ) {
- return !!UndoManager.navigationKeyCodes[ keyCode ];
- };
- /**
- * Returns the group to which the passed `keyCode` belongs.
- *
- * @since 4.4.5
- * @static
- * @param {Number} keyCode
- * @returns {Number}
- */
- UndoManager.getKeyGroup = function( keyCode ) {
- var keyGroups = UndoManager.keyGroups;
- return backspaceOrDelete[ keyCode ] ? keyGroups.FUNCTIONAL : keyGroups.PRINTABLE;
- };
- /**
- * @since 4.4.5
- * @static
- * @param {Number} keyGroup
- * @returns {Number}
- */
- UndoManager.getOppositeKeyGroup = function( keyGroup ) {
- var keyGroups = UndoManager.keyGroups;
- return ( keyGroup == keyGroups.FUNCTIONAL ? keyGroups.PRINTABLE : keyGroups.FUNCTIONAL );
- };
- /**
- * Whether we need to use a workaround for functional (*Backspace*, *Delete*) keys not firing
- * the `keypress` event in Internet Explorer in this environment and for the specified `keyCode`.
- *
- * @since 4.4.5
- * @static
- * @param {Number} keyCode
- * @returns {Boolean}
- */
- UndoManager.ieFunctionalKeysBug = function( keyCode ) {
- return CKEDITOR.env.ie && UndoManager.getKeyGroup( keyCode ) == UndoManager.keyGroups.FUNCTIONAL;
- };
- // Helper method called when undoManager.typing val was changed to true.
- function onTypingStart( undoManager ) {
- // It's safe to now indicate typing state.
- undoManager.typing = true;
- // Manually mark snapshot as available.
- undoManager.hasUndo = true;
- undoManager.hasRedo = false;
- undoManager.onChange();
- }
- /**
- * Contains a snapshot of the editor content and selection at a given point in time.
- *
- * @private
- * @class CKEDITOR.plugins.undo.Image
- * @constructor Creates an Image class instance.
- * @param {CKEDITOR.editor} editor The editor instance on which the image is created.
- * @param {Boolean} [contentsOnly] If set to `true`, the image will only contain content without the selection.
- */
- var Image = CKEDITOR.plugins.undo.Image = function( editor, contentsOnly ) {
- this.editor = editor;
- editor.fire( 'beforeUndoImage' );
- var contents = editor.getSnapshot();
- // In IE, we need to remove the expando attributes.
- if ( CKEDITOR.env.ie && contents )
- contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' );
- this.contents = contents;
- if ( !contentsOnly ) {
- var selection = contents && editor.getSelection();
- this.bookmarks = selection && selection.createBookmarks2( true );
- }
- editor.fire( 'afterUndoImage' );
- };
- // Attributes that browser may changing them when setting via innerHTML.
- var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
- Image.prototype = {
- /**
- * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
- * @returns {Boolean} Returns `true` if content in `otherImage` is the same.
- */
- equalsContent: function( otherImage ) {
- var thisContents = this.contents,
- otherContents = otherImage.contents;
- // For IE7 and IE QM: Comparing only the protected attribute values but not the original ones.(#4522)
- if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {
- thisContents = thisContents.replace( protectedAttrs, '' );
- otherContents = otherContents.replace( protectedAttrs, '' );
- }
- if ( thisContents != otherContents )
- return false;
- return true;
- },
- /**
- * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
- * @returns {Boolean} Returns `true` if selection in `otherImage` is the same.
- */
- equalsSelection: function( otherImage ) {
- var bookmarksA = this.bookmarks,
- bookmarksB = otherImage.bookmarks;
- if ( bookmarksA || bookmarksB ) {
- if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
- return false;
- for ( var i = 0; i < bookmarksA.length; i++ ) {
- var bookmarkA = bookmarksA[ i ],
- bookmarkB = bookmarksB[ i ];
- if ( bookmarkA.startOffset != bookmarkB.startOffset || bookmarkA.endOffset != bookmarkB.endOffset ||
- !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||
- !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) ) {
- return false;
- }
- }
- }
- return true;
- }
- /**
- * Editor content.
- *
- * @readonly
- * @property {String} contents
- */
- /**
- * Bookmarks representing the selection in an image.
- *
- * @readonly
- * @property {Object[]} bookmarks Array of bookmark2 objects, see {@link CKEDITOR.dom.range#createBookmark2} for definition.
- */
- };
- /**
- * A class encapsulating all native event listeners which have to be used in
- * order to handle undo manager integration for native editing actions (excluding drag and drop and paste support
- * handled by the Clipboard plugin).
- *
- * @since 4.4.4
- * @private
- * @class CKEDITOR.plugins.undo.NativeEditingHandler
- * @member CKEDITOR.plugins.undo Undo manager owning the handler.
- * @constructor
- * @param {CKEDITOR.plugins.undo.UndoManager} undoManager
- */
- var NativeEditingHandler = CKEDITOR.plugins.undo.NativeEditingHandler = function( undoManager ) {
- // We'll use keyboard + input events to determine if snapshot should be created.
- // Since `input` event is fired before `keyup`. We can tell in `keyup` event if input occured.
- // That will tell us if any printable data was inserted.
- // On `input` event we'll increase input fired counter for proper key code.
- // Eventually it might be canceled by paste/drop using `ignoreInputEvent` flag.
- // Order of events can be found in http://www.w3.org/TR/DOM-Level-3-Events/
- /**
- * An undo manager instance owning the editing handler.
- *
- * @property {CKEDITOR.plugins.undo.UndoManager} undoManager
- */
- this.undoManager = undoManager;
- /**
- * See {@link #ignoreInputEventListener}.
- *
- * @since 4.4.5
- * @private
- */
- this.ignoreInputEvent = false;
- /**
- * A stack of pressed keys.
- *
- * @since 4.4.5
- * @property {CKEDITOR.plugins.undo.KeyEventsStack} keyEventsStack
- */
- this.keyEventsStack = new KeyEventsStack();
- /**
- * An image of the editor during the `keydown` event (therefore without DOM modification).
- *
- * @property {CKEDITOR.plugins.undo.Image} lastKeydownImage
- */
- this.lastKeydownImage = null;
- };
- NativeEditingHandler.prototype = {
- /**
- * The `keydown` event listener.
- *
- * @param {CKEDITOR.dom.event} evt
- */
- onKeydown: function( evt ) {
- var keyCode = evt.data.getKey();
- // The composition is in progress - ignore the key. (#12597)
- if ( keyCode === 229 ) {
- return;
- }
- // Block undo/redo keystrokes when at the bottom/top of the undo stack (#11126 and #11677).
- if ( CKEDITOR.tools.indexOf( keystrokes, evt.data.getKeystroke() ) > -1 ) {
- evt.data.preventDefault();
- return;
- }
- // Cleaning tab functional keys.
- this.keyEventsStack.cleanUp( evt );
- var undoManager = this.undoManager;
- // Gets last record for provided keyCode. If not found will create one.
- var last = this.keyEventsStack.getLast( keyCode );
- if ( !last ) {
- this.keyEventsStack.push( keyCode );
- }
- // We need to store an image which will be used in case of key group
- // change.
- this.lastKeydownImage = new Image( undoManager.editor );
- if ( UndoManager.isNavigationKey( keyCode ) || this.undoManager.keyGroupChanged( keyCode ) ) {
- if ( undoManager.strokesRecorded[ 0 ] || undoManager.strokesRecorded[ 1 ] ) {
- // We already have image, so we'd like to reuse it.
- // #12300
- undoManager.save( false, this.lastKeydownImage, false );
- undoManager.resetType();
- }
- }
- },
- /**
- * The `input` event listener.
- */
- onInput: function() {
- // Input event is ignored if paste/drop event were fired before.
- if ( this.ignoreInputEvent ) {
- // Reset flag - ignore only once.
- this.ignoreInputEvent = false;
- return;
- }
- var lastInput = this.keyEventsStack.getLast();
- // Nothing in key events stack, but input event called. Interesting...
- // That's because on Android order of events is buggy and also keyCode is set to 0.
- if ( !lastInput ) {
- lastInput = this.keyEventsStack.push( 0 );
- }
- // Increment inputs counter for provided key code.
- this.keyEventsStack.increment( lastInput.keyCode );
- // Exceeded limit.
- if ( this.keyEventsStack.getTotalInputs() >= this.undoManager.strokesLimit ) {
- this.undoManager.type( lastInput.keyCode, true );
- this.keyEventsStack.resetInputs();
- }
- },
- /**
- * The `keyup` event listener.
- *
- * @param {CKEDITOR.dom.event} evt
- */
- onKeyup: function( evt ) {
- var undoManager = this.undoManager,
- keyCode = evt.data.getKey(),
- totalInputs = this.keyEventsStack.getTotalInputs();
- // Remove record from stack for provided key code.
- this.keyEventsStack.remove( keyCode );
- // Second part of the workaround for IEs functional keys bug. We need to check whether something has really
- // changed because we blindly mocked the keypress event.
- // Also we need to be aware that lastKeydownImage might not be available (#12327).
- if ( UndoManager.ieFunctionalKeysBug( keyCode ) && this.lastKeydownImage &&
- this.lastKeydownImage.equalsContent( new Image( undoManager.editor, true ) ) ) {
- return;
- }
- if ( totalInputs > 0 ) {
- undoManager.type( keyCode );
- } else if ( UndoManager.isNavigationKey( keyCode ) ) {
- // Note content snapshot has been checked in keydown.
- this.onNavigationKey( true );
- }
- },
- /**
- * Method called for navigation change. At first it will check if current content does not differ
- * from the last saved snapshot.
- *
- * * If the content is different, the method creates a standard, extra snapshot.
- * * If the content is not different, the method will compare the selection, and will
- * amend the last snapshot selection if it changed.
- *
- * @param {Boolean} skipContentCompare If set to `true`, it will not compare content, and only do a selection check.
- */
- onNavigationKey: function( skipContentCompare ) {
- var undoManager = this.undoManager;
- // We attempt to save content snapshot, if content didn't change, we'll
- // only amend selection.
- if ( skipContentCompare || !undoManager.save( true, null, false ) )
- undoManager.updateSelection( new Image( undoManager.editor ) );
- undoManager.resetType();
- },
- /**
- * Makes the next `input` event to be ignored.
- */
- ignoreInputEventListener: function() {
- this.ignoreInputEvent = true;
- },
- /**
- * Attaches editable listeners required to provide the undo functionality.
- */
- attachListeners: function() {
- var editor = this.undoManager.editor,
- editable = editor.editable(),
- that = this;
- // We'll create a snapshot here (before DOM modification), because we'll
- // need unmodified content when we got keygroup toggled in keyup.
- editable.attachListener( editable, 'keydown', function( evt ) {
- that.onKeydown( evt );
- // On IE keypress isn't fired for functional (backspace/delete) keys.
- // Let's pretend that something's changed.
- if ( UndoManager.ieFunctionalKeysBug( evt.data.getKey() ) ) {
- that.onInput();
- }
- }, null, null, 999 );
- // Only IE can't use input event, because it's not fired in contenteditable.
- editable.attachListener( editable, ( CKEDITOR.env.ie ? 'keypress' : 'input' ), that.onInput, that, null, 999 );
- // Keyup executes main snapshot logic.
- editable.attachListener( editable, 'keyup', that.onKeyup, that, null, 999 );
- // On paste and drop we need to ignore input event.
- // It would result with calling undoManager.type() on any following key.
- editable.attachListener( editable, 'paste', that.ignoreInputEventListener, that, null, 999 );
- editable.attachListener( editable, 'drop', that.ignoreInputEventListener, that, null, 999 );
- // Click should create a snapshot if needed, but shouldn't cause change event.
- // Don't pass onNavigationKey directly as a listener because it accepts one argument which
- // will conflict with evt passed to listener.
- // #12324 comment:4
- editable.attachListener( editable.isInline() ? editable : editor.document.getDocumentElement(), 'click', function() {
- that.onNavigationKey();
- }, null, null, 999 );
- // When pressing `Tab` key while editable is focused, `keyup` event is not fired.
- // Which means that record for `tab` key stays in key events stack.
- // We assume that when editor is blurred `tab` key is already up.
- editable.attachListener( this.undoManager.editor, 'blur', function() {
- that.keyEventsStack.remove( 9 /*Tab*/ );
- }, null, null, 999 );
- }
- };
- /**
- * This class represents a stack of pressed keys and stores information
- * about how many `input` events each key press has caused.
- *
- * @since 4.4.5
- * @private
- * @class CKEDITOR.plugins.undo.KeyEventsStack
- * @constructor
- */
- var KeyEventsStack = CKEDITOR.plugins.undo.KeyEventsStack = function() {
- /**
- * @readonly
- */
- this.stack = [];
- };
- KeyEventsStack.prototype = {
- /**
- * Pushes a literal object with two keys: `keyCode` and `inputs` (whose initial value is set to `0`) to stack.
- * It is intended to be called on the `keydown` event.
- *
- * @param {Number} keyCode
- */
- push: function( keyCode ) {
- var length = this.stack.push( { keyCode: keyCode, inputs: 0 } );
- return this.stack[ length - 1 ];
- },
- /**
- * Returns the index of the last registered `keyCode` in the stack.
- * If no `keyCode` is provided, then the function will return the index of the last item.
- * If an item is not found, it will return `-1`.
- *
- * @param {Number} [keyCode]
- * @returns {Number}
- */
- getLastIndex: function( keyCode ) {
- if ( typeof keyCode != 'number' ) {
- return this.stack.length - 1; // Last index or -1.
- } else {
- var i = this.stack.length;
- while ( i-- ) {
- if ( this.stack[ i ].keyCode == keyCode ) {
- return i;
- }
- }
- return -1;
- }
- },
- /**
- * Returns the last key recorded in the stack. If `keyCode` is provided, then it will return
- * the last record for this `keyCode`.
- *
- * @param {Number} [keyCode]
- * @returns {Object} Last matching record or `null`.
- */
- getLast: function( keyCode ) {
- var index = this.getLastIndex( keyCode );
- if ( index != -1 ) {
- return this.stack[ index ];
- } else {
- return null;
- }
- },
- /**
- * Increments registered input events for stack record for a given `keyCode`.
- *
- * @param {Number} keyCode
- */
- increment: function( keyCode ) {
- var found = this.getLast( keyCode );
- if ( !found ) { // %REMOVE_LINE%
- throw new Error( 'Trying to increment, but could not found by keyCode: ' + keyCode + '.' ); // %REMOVE_LINE%
- } // %REMOVE_LINE%
- found.inputs++;
- },
- /**
- * Removes the last record from the stack for the provided `keyCode`.
- *
- * @param {Number} keyCode
- */
- remove: function( keyCode ) {
- var index = this.getLastIndex( keyCode );
- if ( index != -1 ) {
- this.stack.splice( index, 1 );
- }
- },
- /**
- * Resets the `inputs` value to `0` for a given `keyCode` or in entire stack if a
- * `keyCode` is not specified.
- *
- * @param {Number} [keyCode]
- */
- resetInputs: function( keyCode ) {
- if ( typeof keyCode == 'number' ) {
- var last = this.getLast( keyCode );
- if ( !last ) { // %REMOVE_LINE%
- throw new Error( 'Trying to reset inputs count, but could not found by keyCode: ' + keyCode + '.' ); // %REMOVE_LINE%
- } // %REMOVE_LINE%
- last.inputs = 0;
- } else {
- var i = this.stack.length;
- while ( i-- ) {
- this.stack[ i ].inputs = 0;
- }
- }
- },
- /**
- * Sums up inputs number for each key code and returns it.
- *
- * @returns {Number}
- */
- getTotalInputs: function() {
- var i = this.stack.length,
- total = 0;
- while ( i-- ) {
- total += this.stack[ i ].inputs;
- }
- return total;
- },
- /**
- * Cleans the stack based on a provided `keydown` event object. The rationale behind this method
- * is that some keystrokes cause the `keydown` event to be fired in the editor, but not the `keyup` event.
- * For instance, *Alt+Tab* will fire `keydown`, but since the editor is blurred by it, then there is
- * no `keyup`, so the keystroke is not removed from the stack.
- *
- * @param {CKEDITOR.dom.event} event
- */
- cleanUp: function( event ) {
- var nativeEvent = event.data.$;
- if ( !( nativeEvent.ctrlKey || nativeEvent.metaKey ) ) {
- this.remove( 17 );
- }
- if ( !nativeEvent.shiftKey ) {
- this.remove( 16 );
- }
- if ( !nativeEvent.altKey ) {
- this.remove( 18 );
- }
- }
- };
- } )();
- /**
- * The number of undo steps to be saved. The higher value is set, the more
- * memory is used for it.
- *
- * config.undoStackSize = 50;
- *
- * @cfg {Number} [undoStackSize=20]
- * @member CKEDITOR.config
- */
- /**
- * Fired when the editor is about to save an undo snapshot. This event can be
- * fired by plugins and customizations to make the editor save undo snapshots.
- *
- * @event saveSnapshot
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- */
- /**
- * Fired before an undo image is to be created. An *undo image* represents the
- * editor state at some point. It is saved into the undo store, so the editor is
- * able to recover the editor state on undo and redo operations.
- *
- * @since 3.5.3
- * @event beforeUndoImage
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- * @see CKEDITOR.editor#afterUndoImage
- */
- /**
- * Fired after an undo image is created. An *undo image* represents the
- * editor state at some point. It is saved into the undo store, so the editor is
- * able to recover the editor state on undo and redo operations.
- *
- * @since 3.5.3
- * @event afterUndoImage
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- * @see CKEDITOR.editor#beforeUndoImage
- */
- /**
- * Fired when the content of the editor is changed.
- *
- * Due to performance reasons, it is not verified if the content really changed.
- * The editor instead watches several editing actions that usually result in
- * changes. This event may thus in some cases be fired when no changes happen
- * or may even get fired twice.
- *
- * If it is important not to get the `change` event fired too often, you should compare the
- * previous and the current editor content inside the event listener. It is
- * not recommended to do that on every `change` event.
- *
- * Please note that the `change` event is only fired in the {@link #property-mode wysiwyg mode}.
- * In order to implement similar functionality in the source mode, you can listen for example to the {@link #key}
- * event or the native [`input`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/input)
- * event (not supported by Internet Explorer 8).
- *
- * editor.on( 'mode', function() {
- * if ( this.mode == 'source' ) {
- * var editable = editor.editable();
- * editable.attachListener( editable, 'input', function() {
- * // Handle changes made in the source mode.
- * } );
- * }
- * } );
- *
- * @since 4.2
- * @event change
- * @member CKEDITOR.editor
- * @param {CKEDITOR.editor} editor This editor instance.
- */
|