Skip to content

Commit bd2bb0a

Browse files
SimeonCSimeonC
SimeonC
authored and
SimeonC
committedOct 21, 2014
feat(taBind.undoManager): Add undoManager to taBind.
Fix #166
1 parent af233b9 commit bd2bb0a

11 files changed

+2167
-1728
lines changed
 

‎Gruntfile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ module.exports = function (grunt) {
5151
}
5252
},
5353
jshint: {
54-
files: ['src/textAngular.js', 'src/textAngularSetup.js', 'test/*.spec.js'],// don't hint the textAngularSanitize as they will fail
54+
files: ['src/textAngular.js', 'src/textAngularSetup.js', 'test/*.spec.js', 'test/taBind/*.spec.js'],// don't hint the textAngularSanitize as they will fail
5555
options: {
5656
eqeqeq: true,
5757
immed: true,

‎src/textAngular.js

+154-47
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,7 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
2121
}
2222
}
2323

24-
// fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
25-
// this is set true when a blur occurs as the blur of the ta-bind triggers before the click
26-
var globalContentEditableBlur = false;
27-
/* istanbul ignore next: Browser Un-Focus fix for webkit */
28-
if(/AppleWebKit\/([\d.]+)/.exec(navigator.userAgent)) { // detect webkit
29-
document.addEventListener("click", function(_event){
30-
var e = _event || window.event;
31-
var curelement = e.target;
32-
if(globalContentEditableBlur && curelement !== null){
33-
var isEditable = false;
34-
var tempEl = curelement;
35-
while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
36-
isEditable = tempEl.contentEditable === 'true';
37-
tempEl = tempEl.parentNode;
38-
}
39-
if(!isEditable){
40-
document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
41-
curelement.focus(); // focus the wanted element.
42-
}
43-
}
44-
globalContentEditableBlur = false;
45-
}, false); // add global click handler
46-
angular.element(document).ready(function () {
47-
angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" style="width:1px;height:1px;border:none;margin:0;padding:0;position:absolute; top: -10000px; left: -10000px;" unselectable="on" tabIndex="-1">'));
48-
});
49-
}
50-
51-
// Gloabl to textAngular REGEXP vars for block and list elements.
52-
53-
var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/ig;
54-
var LISTELEMENTS = /^(ul|li|ol)$/ig;
24+
5525
// IE version detection - http://stackoverflow.com/questions/4169160/javascript-ie-detection-why-not-use-simple-conditional-comments
5626
// We need this as IE sometimes plays funny tricks with the contenteditable.
5727
// ----------------------------------------------------------
@@ -84,7 +54,42 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
8454

8555
return ((rv > -1) ? rv : undef);
8656
}());
87-
57+
// detect webkit
58+
var webkit = /AppleWebKit\/([\d.]+)/.test(navigator.userAgent);
59+
60+
// fix a webkit bug, see: https://gist.github.com/shimondoodkin/1081133
61+
// this is set true when a blur occurs as the blur of the ta-bind triggers before the click
62+
var globalContentEditableBlur = false;
63+
/* istanbul ignore next: Browser Un-Focus fix for webkit */
64+
if(webkit) {
65+
document.addEventListener("click", function(_event){
66+
var e = _event || window.event;
67+
var curelement = e.target;
68+
if(globalContentEditableBlur && curelement !== null){
69+
var isEditable = false;
70+
var tempEl = curelement;
71+
while(tempEl !== null && tempEl.tagName.toLowerCase() !== 'html' && !isEditable){
72+
isEditable = tempEl.contentEditable === 'true';
73+
tempEl = tempEl.parentNode;
74+
}
75+
if(!isEditable){
76+
document.getElementById('textAngular-editableFix-010203040506070809').setSelectionRange(0, 0); // set caret focus to an element that handles caret focus correctly.
77+
curelement.focus(); // focus the wanted element.
78+
}
79+
}
80+
globalContentEditableBlur = false;
81+
}, false); // add global click handler
82+
angular.element(document).ready(function () {
83+
angular.element(document.body).append(angular.element('<input id="textAngular-editableFix-010203040506070809" style="width:1px;height:1px;border:none;margin:0;padding:0;position:absolute; top: -10000px; left: -10000px;" unselectable="on" tabIndex="-1">'));
84+
});
85+
}
86+
87+
// Gloabl to textAngular REGEXP vars for block and list elements.
88+
89+
var BLOCKELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/ig;
90+
var LISTELEMENTS = /^(ul|li|ol)$/ig;
91+
var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/ig;
92+
8893
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility
8994
/* istanbul ignore next: trim shim for older browsers */
9095
if (!String.prototype.trim) {
@@ -283,15 +288,21 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
283288
angular.extend(scope, angular.copy(taOptions), {
284289
// wraps the selection in the provided tag / execCommand function. Should only be called in WYSIWYG mode.
285290
wrapSelection: function(command, opt, isSelectableElementTool){
286-
// catch errors like FF erroring when you try to force an undo with nothing done
287-
_taExecCommand(command, false, opt);
288-
if(isSelectableElementTool){
289-
// re-apply the selectable tool events
290-
scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
291+
if(command.toLowerCase() === "undo"){
292+
scope['$undoTaBindtaTextElement' + _serial]();
293+
}else if(command.toLowerCase() === "redo"){
294+
scope['$redoTaBindtaTextElement' + _serial]();
295+
}else{
296+
// catch errors like FF erroring when you try to force an undo with nothing done
297+
_taExecCommand(command, false, opt);
298+
if(isSelectableElementTool){
299+
// re-apply the selectable tool events
300+
scope['reApplyOnSelectorHandlerstaTextElement' + _serial]();
301+
}
302+
// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
303+
// You still have focus on the text/html input it just doesn't show up
304+
scope.displayElements.text[0].focus();
291305
}
292-
// refocus on the shown display element, this fixes a display bug when using :focus styles to outline the box.
293-
// You still have focus on the text/html input it just doesn't show up
294-
scope.displayElements.text[0].focus();
295306
},
296307
showHtml: false
297308
});
@@ -999,7 +1010,9 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
9991010
var _isReadonly = false;
10001011
var _focussed = false;
10011012
var _disableSanitizer = attrs.taUnsafeSanitizer || taOptions.disableSanitizer;
1002-
var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|46|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/;
1013+
var _lastKey;
1014+
var BLOCKED_KEYS = /^(9|19|20|27|33|34|35|36|37|38|39|40|45|112|113|114|115|116|117|118|119|120|121|122|123|144|145)$/;
1015+
var UNDO_TRIGGER_KEYS = /^(8|13|32|46|59|61|107|109|186|187|188|189|190|191|192|219|220|221|222)$/; // spaces, enter, delete, backspace, all punctuation
10031016

10041017
// defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
10051018
// non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
@@ -1026,21 +1039,90 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
10261039
}
10271040

10281041
element.addClass('ta-bind');
1029-
1042+
1043+
var _undoKeyupTimeout;
1044+
1045+
scope['$undoManager' + (attrs.id || '')] = ngModel.$undoManager = {
1046+
_stack: [],
1047+
_index: 0,
1048+
_max: 1000,
1049+
push: function(value){
1050+
if((typeof value === "undefined" || value === null) ||
1051+
((typeof this.current() !== "undefined" && this.current() !== null) && value === this.current())) return value;
1052+
if(this._index < this._stack.length - 1){
1053+
this._stack = this._stack.slice(0,this._index+1);
1054+
}
1055+
this._stack.push(value);
1056+
if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
1057+
if(this._stack.length > this._max) this._stack.shift();
1058+
this._index = this._stack.length - 1;
1059+
return value;
1060+
},
1061+
undo: function(){
1062+
return this.setToIndex(this._index-1);
1063+
},
1064+
redo: function(){
1065+
return this.setToIndex(this._index+1);
1066+
},
1067+
setToIndex: function(index){
1068+
if(index < 0 || index > this._stack.length - 1){
1069+
return undefined;
1070+
}
1071+
this._index = index;
1072+
return this.current();
1073+
},
1074+
current: function(){
1075+
return this._stack[this._index];
1076+
}
1077+
};
1078+
1079+
var _undo = scope['$undoTaBind' + (attrs.id || '')] = function(){
1080+
/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
1081+
if(!_isReadonly && _isContentEditable){
1082+
var content = ngModel.$undoManager.undo();
1083+
if(typeof content !== "undefined" && content !== null){
1084+
element[0].innerHTML = content;
1085+
_setViewValue(content, false);
1086+
/* istanbul ignore else: browser catch */
1087+
if(element[0].childNodes.length) taSelection.setSelectionToElementEnd(element[0].childNodes[element[0].childNodes.length-1]);
1088+
else taSelection.setSelectionToElementEnd(element[0]);
1089+
}
1090+
}
1091+
};
1092+
1093+
var _redo = scope['$redoTaBind' + (attrs.id || '')] = function(){
1094+
/* istanbul ignore else: can't really test it due to all changes being ignored as well in readonly */
1095+
if(!_isReadonly && _isContentEditable){
1096+
var content = ngModel.$undoManager.redo();
1097+
if(typeof content !== "undefined" && content !== null){
1098+
element[0].innerHTML = content;
1099+
_setViewValue(content, false);
1100+
/* istanbul ignore else: browser catch */
1101+
if(element[0].childNodes.length) taSelection.setSelectionToElementEnd(element[0].childNodes[element[0].childNodes.length-1]);
1102+
else taSelection.setSelectionToElementEnd(element[0]);
1103+
}
1104+
}
1105+
};
1106+
10301107
// in here we are undoing the converts used elsewhere to prevent the < > and & being displayed when they shouldn't in the code.
10311108
var _compileHtml = function(){
10321109
if(_isContentEditable) return element[0].innerHTML;
10331110
if(_isInputFriendly) return element.val();
10341111
throw ('textAngular Error: attempting to update non-editable taBind');
10351112
};
10361113

1037-
var _setViewValue = function(val){
1114+
var _setViewValue = function(val, triggerUndo){
1115+
if(typeof triggerUndo === "undefined" || triggerUndo === null) triggerUndo = true && _isContentEditable; // if not contentEditable then the native undo/redo is fine
10381116
if(!val) val = _compileHtml();
1039-
if(val === _defaultTest || val.match(_trimTest)){
1117+
if(val === _defaultTest || _trimTest.test(val)){
10401118
// this avoids us from tripping the ng-pristine flag if we click in and out with out typing
10411119
if(ngModel.$viewValue !== '') ngModel.$setViewValue('');
1120+
if(triggerUndo && ngModel.$undoManager.current() !== '') ngModel.$undoManager.push('');
10421121
}else{
1043-
if(ngModel.$viewValue !== val) ngModel.$setViewValue(val);
1122+
if(ngModel.$viewValue !== val){
1123+
ngModel.$setViewValue(val);
1124+
if(triggerUndo) ngModel.$undoManager.push(val);
1125+
}
10441126
}
10451127
};
10461128

@@ -1184,16 +1266,35 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
11841266
else e.preventDefault();
11851267
});
11861268

1269+
element.on('keydown', function(event, eventData){
1270+
/* istanbul ignore else: this is for catching the jqLite testing*/
1271+
if(eventData) angular.extend(event, eventData);
1272+
/* istanbul ignore else: readonly check */
1273+
if(!_isReadonly){
1274+
if(event.metaKey || event.ctrlKey){
1275+
// covers ctrl/command + z
1276+
if((event.keyCode === 90 && !event.shiftKey)){
1277+
_undo();
1278+
event.preventDefault();
1279+
// covers ctrl + y, command + shift + z
1280+
}else if((event.keyCode === 90 && event.shiftKey) || (event.keyCode === 89 && !event.shiftKey)){
1281+
_redo();
1282+
event.preventDefault();
1283+
}
1284+
}
1285+
}
1286+
});
1287+
11871288
element.on('keyup', function(event, eventData){
11881289
/* istanbul ignore else: this is for catching the jqLite testing*/
11891290
if(eventData) angular.extend(event, eventData);
1291+
if(_undoKeyupTimeout) $timeout.cancel(_undoKeyupTimeout);
11901292
if(!_isReadonly && !BLOCKED_KEYS.test(event.keyCode)){
11911293
// if enter - insert new taDefaultWrap, if shift+enter insert <br/>
11921294
if(_defaultVal !== '' && event.keyCode === 13){
11931295
if(!event.shiftKey){
11941296
// new paragraph, br should be caught correctly
11951297
var selection = taSelection.getSelectionElement();
1196-
var VALIDELEMENTS = /^(address|article|aside|audio|blockquote|canvas|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/ig;
11971298
while(!selection.tagName.match(VALIDELEMENTS) && selection !== element[0]){
11981299
selection = selection.parentNode;
11991300
}
@@ -1209,7 +1310,10 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
12091310
element[0].innerHTML = _defaultVal;
12101311
taSelection.setSelectionToElementStart(element.children()[0]);
12111312
}
1212-
_setViewValue(val);
1313+
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
1314+
_setViewValue(val, triggerUndo);
1315+
if(!triggerUndo) _undoKeyupTimeout = $timeout(function(){ ngModel.$undoManager.push(val); }, 250);
1316+
_lastKey = event.keyCode;
12131317
}
12141318
});
12151319

@@ -1264,6 +1368,9 @@ See README.md or https://github.com/fraywing/textAngular/wiki for requirements a
12641368
// because textAngular is bi-directional (which is awesome) we need to also sanitize values going in from the server
12651369
ngModel.$formatters.push(_sanitize);
12661370
ngModel.$formatters.push(_validity);
1371+
ngModel.$formatters.push(function(value){
1372+
return ngModel.$undoManager.push(value || '');
1373+
});
12671374

12681375
var selectorClickHandler = function(event){
12691376
// emit the element-select event, pass the element

0 commit comments

Comments
 (0)