MDL-44759 atto_undo: Buttons are only enabled when needed

Before the patch it was happening that the undo or redo buttons
were enabled even though clicking on them didn't produce anything.
This is partly because we were not storing the initial value, we
allowed for the stack to be empty, and we were using not clean
HTML.
This commit is contained in:
Frederic Massart 2014-03-27 19:14:16 +08:00
parent ad32dda90c
commit adb2473a2b
4 changed files with 510 additions and 142 deletions

View File

@ -25,6 +25,8 @@ YUI.add('moodle-atto_undo-button', function (Y, NAME) {
* @module moodle-atto_undo-button
*/
var LOGNAME = 'moodle-atto_undo-button';
/**
* Atto text editor undo plugin.
*
@ -86,34 +88,168 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
keys: 89
});
this.get('host').on('atto:selectionchanged', this._changeListener, this);
// Enable the undo once everything has loaded.
this.get('host').on('pluginsloaded', function() {
// Adds the current value to the stack.
this._addToUndo(this._getHTML());
this.get('host').on('atto:selectionchanged', this._changeListener, this);
}, this);
this._updateButtonsStates();
},
/**
* Adds an element to the redo stack.
*
* @method _addToRedo
* @private
* @param {String} html The HTML content to save.
*/
_addToRedo: function(html) {
this._redoStack.push(html);
},
/**
* Adds an element to the undo stack.
*
* @method _addToUndo
* @private
* @param {String} html The HTML content to save.
* @param {Boolean} [clearRedo=false] Whether or not we should clear the redo stack.
*/
_addToUndo: function(html, clearRedo) {
var last = this._undoStack[this._undoStack.length - 1];
if (typeof clearRedo === 'undefined') {
clearRedo = false;
}
if (typeof last === 'undefined') {
Y.log('Oops, nothing was in the undo stack! There should always be something in there.', 'warn', LOGNAME);
}
if (last !== html) {
this._undoStack.push(html);
if (clearRedo) {
this._redoStack = [];
}
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
},
/**
* Get the editor HTML.
*
* @method _getHTML
* @private
* @return {String} The HTML.
*/
_getHTML: function() {
return this.get('host').getCleanHTML();
},
/**
* Get an element on the redo stack.
*
* @method _getRedo
* @private
* @return {String} The HTML to restore, or undefined.
*/
_getRedo: function() {
return this._redoStack.pop();
},
/**
* Get an element on the undo stack.
*
* @method _getUndo
* @private
* @param {String} current The current HTML.
* @return {String} The HTML to restore.
*/
_getUndo: function(current) {
if (this._undoStack.length === 1) {
return this._undoStack[0];
}
last = this._undoStack.pop();
if (last === current) {
// Oops, the latest undo step is the current content, we should unstack once more.
// There is no need to do that in a loop as the same stack should never contain duplicates.
last = this._undoStack.pop();
}
// We always need to keep the first element of the stack.
if (this._undoStack.length === 0) {
this._addToUndo(last);
}
return last;
},
/**
* Restore a value from a stack.
*
* @method _restoreValue
* @private
* @param {String} html The HTML to restore in the editor.
*/
_restoreValue: function(html) {
this.editor.setHTML(html);
// We always add the restored value to the stack, otherwise an event could think that
// the content has changed and clear the redo stack.
this._addToUndo(html);
},
/**
* Update the states of the buttons.
*
* @method _updateButtonsStates
* @private
*/
_updateButtonsStates: function() {
if (this._undoStack.length > 1) {
this.enableButtons('undo');
} else {
this.disableButtons('undo');
}
if (this._redoStack.length > 0) {
this.enableButtons('redo');
} else {
this.disableButtons('redo');
}
},
/**
* Handle a click on undo
*
* @method _undoHandler
* @param {Event} The click event
* @private
*/
_undoHandler: function() {
var html = this.editor.getHTML();
_undoHandler: function(e) {
e.preventDefault();
var html = this._getHTML(),
undo = this._getUndo(html);
this._redoStack.push(html);
var last = this._undoStack.pop();
if (last === html) {
last = this._undoStack.pop();
}
if (last) {
this.editor.setHTML(last);
// Put it back in the undo stack so a new event wont clear the redo stack.
this._undoStack.push(last);
this.highlightButtons('redo');
// Edge case, but that could happen. We do nothing when the content equals the undo step.
if (html === undo) {
this._updateButtonsStates();
return;
}
if (this._undoStack.length === 0) {
// If there are no undos left, unhighlight the undo button.
this.unHighlightButtons('undo');
}
// Restore the value.
this._restoreValue(undo);
// Add to the redo stack.
this._addToRedo(html);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -125,12 +261,19 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
*/
_redoHandler: function(e) {
e.preventDefault();
var html = this.editor.getHTML();
var html = this._getHTML(),
redo = this._getRedo();
this._undoStack.push(html);
var last = this._redoStack.pop();
this.editor.setHTML(last);
this._undoStack.push(last);
// Edge case, but that could happen. We do nothing when the content equals the redo step.
if (html === redo) {
this._updateButtonsStates();
return;
}
// Restore the value.
this._restoreValue(redo);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -144,36 +287,16 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
if (e.event.type.indexOf('key') !== -1) {
// These are the 4 arrow keys.
if ((e.event.keyCode !== 39) &&
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
// Skip this event type. We only want focus/mouse/arrow events.
return;
}
}
if (typeof this._undoStack === 'undefined') {
this._undoStack = [];
}
var last = this._undoStack[this._undoStack.length-1];
var html = this.editor.getHTML();
if (last !== html) {
this._undoStack.push(this.editor.getHTML());
this._redoStack = [];
this.unHighlightButtons('redo');
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
// Show in the buttons if undo/redo is possible.
if (this._undoStack.length) {
this.highlightButtons('undo');
} else {
this.unHighlightButtons('undo');
}
this._addToUndo(this._getHTML(), true);
this._updateButtonsStates();
}
});

View File

@ -1 +1 @@
YUI.add("moodle-atto_undo-button",function(e,t){e.namespace("M.atto_undo").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_maxUndos:40,_undoStack:null,_redoStack:null,initializer:function(){this._undoStack=[],this._redoStack=[],this.addButton({icon:"e/undo",callback:this._undoHandler,buttonName:"undo",keys:90}),this.addButton({icon:"e/redo",callback:this._redoHandler,buttonName:"redo",keys:89}),this.get("host").on("atto:selectionchanged",this._changeListener,this)},_undoHandler:function(){var e=this.editor.getHTML();this._redoStack.push(e);var t=this._undoStack.pop();t===e&&(t=this._undoStack.pop()),t&&(this.editor.setHTML(t),this._undoStack.push(t),this.highlightButtons("redo")),this._undoStack.length===0&&this.unHighlightButtons("undo")},_redoHandler:function(e){e.preventDefault();var t=this.editor.getHTML();this._undoStack.push(t);var n=this._redoStack.pop();this.editor.setHTML(n),this._undoStack.push(n)},_changeListener:function(e){if(e.event.type.indexOf("key")!==-1&&e.event.keyCode!==39&&e.event.keyCode!==37&&e.event.keyCode!==40&&e.event.keyCode!==38)return;typeof this._undoStack=="undefined"&&(this._undoStack=[]);var t=this._undoStack[this._undoStack.length-1],n=this.editor.getHTML();t!==n&&(this._undoStack.push(this.editor.getHTML()),this._redoStack=[],this.unHighlightButtons("redo"));while(this._undoStack.length>this._maxUndos)this._undoStack.shift();this._undoStack.length?this.highlightButtons("undo"):this.unHighlightButtons("undo")}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});
YUI.add("moodle-atto_undo-button",function(e,t){var n="moodle-atto_undo-button";e.namespace("M.atto_undo").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_maxUndos:40,_undoStack:null,_redoStack:null,initializer:function(){this._undoStack=[],this._redoStack=[],this.addButton({icon:"e/undo",callback:this._undoHandler,buttonName:"undo",keys:90}),this.addButton({icon:"e/redo",callback:this._redoHandler,buttonName:"redo",keys:89}),this.get("host").on("pluginsloaded",function(){this._addToUndo(this._getHTML()),this.get("host").on("atto:selectionchanged",this._changeListener,this)},this),this._updateButtonsStates()},_addToRedo:function(e){this._redoStack.push(e)},_addToUndo:function(e,t){var n=this._undoStack[this._undoStack.length-1];typeof t=="undefined"&&(t=!1),typeof n=="undefined",n!==e&&(this._undoStack.push(e),t&&(this._redoStack=[]));while(this._undoStack.length>this._maxUndos)this._undoStack.shift()},_getHTML:function(){return this.get("host").getCleanHTML()},_getRedo:function(){return this._redoStack.pop()},_getUndo:function(e){return this._undoStack.length===1?this._undoStack[0]:(last=this._undoStack.pop(),last===e&&(last=this._undoStack.pop()),this._undoStack.length===0&&this._addToUndo(last),last)},_restoreValue:function(e){this.editor.setHTML(e),this._addToUndo(e)},_updateButtonsStates:function(){this._undoStack.length>1?this.enableButtons("undo"):this.disableButtons("undo"),this._redoStack.length>0?this.enableButtons("redo"):this.disableButtons("redo")},_undoHandler:function(e){e.preventDefault();var t=this._getHTML(),n=this._getUndo(t);if(t===n){this._updateButtonsStates();return}this._restoreValue(n),this._addToRedo(t),this._updateButtonsStates()},_redoHandler:function(e){e.preventDefault();var t=this._getHTML(),n=this._getRedo();if(t===n){this._updateButtonsStates();return}this._restoreValue(n),this._updateButtonsStates()},_changeListener:function(e){if(e.event.type.indexOf("key")!==-1&&e.event.keyCode!==39&&e.event.keyCode!==37&&e.event.keyCode!==40&&e.event.keyCode!==38)return;this._addToUndo(this._getHTML(),!0),this._updateButtonsStates()}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});

View File

@ -25,6 +25,8 @@ YUI.add('moodle-atto_undo-button', function (Y, NAME) {
* @module moodle-atto_undo-button
*/
var LOGNAME = 'moodle-atto_undo-button';
/**
* Atto text editor undo plugin.
*
@ -86,34 +88,167 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
keys: 89
});
this.get('host').on('atto:selectionchanged', this._changeListener, this);
// Enable the undo once everything has loaded.
this.get('host').on('pluginsloaded', function() {
// Adds the current value to the stack.
this._addToUndo(this._getHTML());
this.get('host').on('atto:selectionchanged', this._changeListener, this);
}, this);
this._updateButtonsStates();
},
/**
* Adds an element to the redo stack.
*
* @method _addToRedo
* @private
* @param {String} html The HTML content to save.
*/
_addToRedo: function(html) {
this._redoStack.push(html);
},
/**
* Adds an element to the undo stack.
*
* @method _addToUndo
* @private
* @param {String} html The HTML content to save.
* @param {Boolean} [clearRedo=false] Whether or not we should clear the redo stack.
*/
_addToUndo: function(html, clearRedo) {
var last = this._undoStack[this._undoStack.length - 1];
if (typeof clearRedo === 'undefined') {
clearRedo = false;
}
if (typeof last === 'undefined') {
}
if (last !== html) {
this._undoStack.push(html);
if (clearRedo) {
this._redoStack = [];
}
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
},
/**
* Get the editor HTML.
*
* @method _getHTML
* @private
* @return {String} The HTML.
*/
_getHTML: function() {
return this.get('host').getCleanHTML();
},
/**
* Get an element on the redo stack.
*
* @method _getRedo
* @private
* @return {String} The HTML to restore, or undefined.
*/
_getRedo: function() {
return this._redoStack.pop();
},
/**
* Get an element on the undo stack.
*
* @method _getUndo
* @private
* @param {String} current The current HTML.
* @return {String} The HTML to restore.
*/
_getUndo: function(current) {
if (this._undoStack.length === 1) {
return this._undoStack[0];
}
last = this._undoStack.pop();
if (last === current) {
// Oops, the latest undo step is the current content, we should unstack once more.
// There is no need to do that in a loop as the same stack should never contain duplicates.
last = this._undoStack.pop();
}
// We always need to keep the first element of the stack.
if (this._undoStack.length === 0) {
this._addToUndo(last);
}
return last;
},
/**
* Restore a value from a stack.
*
* @method _restoreValue
* @private
* @param {String} html The HTML to restore in the editor.
*/
_restoreValue: function(html) {
this.editor.setHTML(html);
// We always add the restored value to the stack, otherwise an event could think that
// the content has changed and clear the redo stack.
this._addToUndo(html);
},
/**
* Update the states of the buttons.
*
* @method _updateButtonsStates
* @private
*/
_updateButtonsStates: function() {
if (this._undoStack.length > 1) {
this.enableButtons('undo');
} else {
this.disableButtons('undo');
}
if (this._redoStack.length > 0) {
this.enableButtons('redo');
} else {
this.disableButtons('redo');
}
},
/**
* Handle a click on undo
*
* @method _undoHandler
* @param {Event} The click event
* @private
*/
_undoHandler: function() {
var html = this.editor.getHTML();
_undoHandler: function(e) {
e.preventDefault();
var html = this._getHTML(),
undo = this._getUndo(html);
this._redoStack.push(html);
var last = this._undoStack.pop();
if (last === html) {
last = this._undoStack.pop();
}
if (last) {
this.editor.setHTML(last);
// Put it back in the undo stack so a new event wont clear the redo stack.
this._undoStack.push(last);
this.highlightButtons('redo');
// Edge case, but that could happen. We do nothing when the content equals the undo step.
if (html === undo) {
this._updateButtonsStates();
return;
}
if (this._undoStack.length === 0) {
// If there are no undos left, unhighlight the undo button.
this.unHighlightButtons('undo');
}
// Restore the value.
this._restoreValue(undo);
// Add to the redo stack.
this._addToRedo(html);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -125,12 +260,19 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
*/
_redoHandler: function(e) {
e.preventDefault();
var html = this.editor.getHTML();
var html = this._getHTML(),
redo = this._getRedo();
this._undoStack.push(html);
var last = this._redoStack.pop();
this.editor.setHTML(last);
this._undoStack.push(last);
// Edge case, but that could happen. We do nothing when the content equals the redo step.
if (html === redo) {
this._updateButtonsStates();
return;
}
// Restore the value.
this._restoreValue(redo);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -144,36 +286,16 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
if (e.event.type.indexOf('key') !== -1) {
// These are the 4 arrow keys.
if ((e.event.keyCode !== 39) &&
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
// Skip this event type. We only want focus/mouse/arrow events.
return;
}
}
if (typeof this._undoStack === 'undefined') {
this._undoStack = [];
}
var last = this._undoStack[this._undoStack.length-1];
var html = this.editor.getHTML();
if (last !== html) {
this._undoStack.push(this.editor.getHTML());
this._redoStack = [];
this.unHighlightButtons('redo');
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
// Show in the buttons if undo/redo is possible.
if (this._undoStack.length) {
this.highlightButtons('undo');
} else {
this.unHighlightButtons('undo');
}
this._addToUndo(this._getHTML(), true);
this._updateButtonsStates();
}
});

View File

@ -23,6 +23,8 @@
* @module moodle-atto_undo-button
*/
var LOGNAME = 'moodle-atto_undo-button';
/**
* Atto text editor undo plugin.
*
@ -84,34 +86,168 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
keys: 89
});
this.get('host').on('atto:selectionchanged', this._changeListener, this);
// Enable the undo once everything has loaded.
this.get('host').on('pluginsloaded', function() {
// Adds the current value to the stack.
this._addToUndo(this._getHTML());
this.get('host').on('atto:selectionchanged', this._changeListener, this);
}, this);
this._updateButtonsStates();
},
/**
* Adds an element to the redo stack.
*
* @method _addToRedo
* @private
* @param {String} html The HTML content to save.
*/
_addToRedo: function(html) {
this._redoStack.push(html);
},
/**
* Adds an element to the undo stack.
*
* @method _addToUndo
* @private
* @param {String} html The HTML content to save.
* @param {Boolean} [clearRedo=false] Whether or not we should clear the redo stack.
*/
_addToUndo: function(html, clearRedo) {
var last = this._undoStack[this._undoStack.length - 1];
if (typeof clearRedo === 'undefined') {
clearRedo = false;
}
if (typeof last === 'undefined') {
Y.log('Oops, nothing was in the undo stack! There should always be something in there.', 'warn', LOGNAME);
}
if (last !== html) {
this._undoStack.push(html);
if (clearRedo) {
this._redoStack = [];
}
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
},
/**
* Get the editor HTML.
*
* @method _getHTML
* @private
* @return {String} The HTML.
*/
_getHTML: function() {
return this.get('host').getCleanHTML();
},
/**
* Get an element on the redo stack.
*
* @method _getRedo
* @private
* @return {String} The HTML to restore, or undefined.
*/
_getRedo: function() {
return this._redoStack.pop();
},
/**
* Get an element on the undo stack.
*
* @method _getUndo
* @private
* @param {String} current The current HTML.
* @return {String} The HTML to restore.
*/
_getUndo: function(current) {
if (this._undoStack.length === 1) {
return this._undoStack[0];
}
last = this._undoStack.pop();
if (last === current) {
// Oops, the latest undo step is the current content, we should unstack once more.
// There is no need to do that in a loop as the same stack should never contain duplicates.
last = this._undoStack.pop();
}
// We always need to keep the first element of the stack.
if (this._undoStack.length === 0) {
this._addToUndo(last);
}
return last;
},
/**
* Restore a value from a stack.
*
* @method _restoreValue
* @private
* @param {String} html The HTML to restore in the editor.
*/
_restoreValue: function(html) {
this.editor.setHTML(html);
// We always add the restored value to the stack, otherwise an event could think that
// the content has changed and clear the redo stack.
this._addToUndo(html);
},
/**
* Update the states of the buttons.
*
* @method _updateButtonsStates
* @private
*/
_updateButtonsStates: function() {
if (this._undoStack.length > 1) {
this.enableButtons('undo');
} else {
this.disableButtons('undo');
}
if (this._redoStack.length > 0) {
this.enableButtons('redo');
} else {
this.disableButtons('redo');
}
},
/**
* Handle a click on undo
*
* @method _undoHandler
* @param {Event} The click event
* @private
*/
_undoHandler: function() {
var html = this.editor.getHTML();
_undoHandler: function(e) {
e.preventDefault();
var html = this._getHTML(),
undo = this._getUndo(html);
this._redoStack.push(html);
var last = this._undoStack.pop();
if (last === html) {
last = this._undoStack.pop();
}
if (last) {
this.editor.setHTML(last);
// Put it back in the undo stack so a new event wont clear the redo stack.
this._undoStack.push(last);
this.highlightButtons('redo');
// Edge case, but that could happen. We do nothing when the content equals the undo step.
if (html === undo) {
this._updateButtonsStates();
return;
}
if (this._undoStack.length === 0) {
// If there are no undos left, unhighlight the undo button.
this.unHighlightButtons('undo');
}
// Restore the value.
this._restoreValue(undo);
// Add to the redo stack.
this._addToRedo(html);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -123,12 +259,19 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
*/
_redoHandler: function(e) {
e.preventDefault();
var html = this.editor.getHTML();
var html = this._getHTML(),
redo = this._getRedo();
this._undoStack.push(html);
var last = this._redoStack.pop();
this.editor.setHTML(last);
this._undoStack.push(last);
// Edge case, but that could happen. We do nothing when the content equals the redo step.
if (html === redo) {
this._updateButtonsStates();
return;
}
// Restore the value.
this._restoreValue(redo);
// Update the button states.
this._updateButtonsStates();
},
/**
@ -142,35 +285,15 @@ Y.namespace('M.atto_undo').Button = Y.Base.create('button', Y.M.editor_atto.Edit
if (e.event.type.indexOf('key') !== -1) {
// These are the 4 arrow keys.
if ((e.event.keyCode !== 39) &&
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
(e.event.keyCode !== 37) &&
(e.event.keyCode !== 40) &&
(e.event.keyCode !== 38)) {
// Skip this event type. We only want focus/mouse/arrow events.
return;
}
}
if (typeof this._undoStack === 'undefined') {
this._undoStack = [];
}
var last = this._undoStack[this._undoStack.length-1];
var html = this.editor.getHTML();
if (last !== html) {
this._undoStack.push(this.editor.getHTML());
this._redoStack = [];
this.unHighlightButtons('redo');
}
while (this._undoStack.length > this._maxUndos) {
this._undoStack.shift();
}
// Show in the buttons if undo/redo is possible.
if (this._undoStack.length) {
this.highlightButtons('undo');
} else {
this.unHighlightButtons('undo');
}
this._addToUndo(this._getHTML(), true);
this._updateButtonsStates();
}
});