LP#1402797 Global undo/redo stack stack
authorMike Rylander <mrylander@gmail.com>
Thu, 12 Feb 2015 00:23:32 +0000 (19:23 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 25 Feb 2015 16:16:09 +0000 (11:16 -0500)
Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
Open-ILS/web/js/ui/default/staff/marcrecord.js

index 8f53a54..6b517ab 100644 (file)
@@ -25,7 +25,6 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             $scope.$parent.$parent.content = replace_with
                         })
                     }, 0);
-                    console.log('well, replaced it');
                     $($element).parent().css({display: 'none'});
                 }
             }
@@ -36,16 +35,17 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 .directive("egMarcEditEditable", ['$timeout', '$compile', '$document', function ($timeout, $compile, $document) {
     return {
         restrict: 'E',
-        replace: false,
-        transclude: true,
+        replace: true,
         template: '<input style="font-family: \'Lucida Console\', Monaco, monospace;" ng-model="content" size="{{content.length * 1.1}}" maxlength="{{max}}" class="" type="text"/>',
         scope: {
             field: '=',
+            onKeydown: '=',
             subfield: '=',
             content: '=',
             contextItemContainer: '@',
+            idPath: '=',
             max: '@',
-            type: '@'
+            itype: '@'
         },
         controller : ['$scope',
             function ( $scope ) {
@@ -71,6 +71,10 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     if ($scope.context_menu_element) {
                         console.log('Reshowing context menu...');
                         $($scope.context_menu_element).css({ display: 'block', top: event.pageY, left: event.pageX });
+                        $('body').on('click.context_menu',function() {
+                            $($scope.context_menu_element).css('display','none');
+                            $('body').off('click.context_menu');
+                        });
                         return false;
                     }
 
@@ -83,26 +87,26 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             '</ul>';
             
                         var tnode = angular.element(tmpl);
-                        console.log('... got element ...');
-
                         $document.find('body').append(tnode);
-                        console.log('... attached to DOM ...');
 
                         $(tnode).css({
                             display: 'block',
                             top: event.pageY,
                             left: event.pageX
                         });
-                        console.log('... displayed ...');
 
                         $scope.context_menu_element = tnode;
-                        console.log('... captured for later ...');
 
                         $timeout(function() {
                             var e = $compile(tnode)($scope);
-                            console.log('... compiled: ' + e);
                         }, 0);
 
+
+                        $('body').on('click.context_menu',function() {
+                            $(tnode).css('display','none');
+                            $('body').off('click.context_menu');
+                        });
+
                         return false;
                     }
             
@@ -113,7 +117,10 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
         ],
         link: function (scope, element, attrs) {
 
+            if (scope.onKeydown) element.bind('keydown', scope.onKeydown);
+
             element.bind('change', function (e) { element.size = scope.max || parseInt(scope.content.length * 1.1) });
+
             if (scope.contextItemContainer && angular.isArray(scope[scope.contextItemContainer]))
                 element.bind('contextmenu', scope.showContext);
         }
@@ -122,71 +129,94 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
 .directive("egMarcEditSubfield", function () {
     return {
+        transclude: true,
         restrict: 'E',
         template: '<span>'+
-                    '<span><eg-marc-edit-editable type="sfc" class="marcsfcode" field="field" subfield="subfield" content="subfield[0]" max="1"/></span>'+
-                    '<span><eg-marc-edit-editable type="sfv" class="marcsfvalue" field="field" subfield="subfield" content="subfield[1]"/></span>'+
+                    '<span><eg-marc-edit-editable '+
+                        'itype="sfc" '+
+                        'class="marcsfcode" '+
+                        'field="field" '+
+                        'subfield="subfield" '+
+                        'content="subfield[0]" '+
+                        'max="1" '+
+                        'on-keydown="onKeydown" '+
+                        'id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}s{{subfield[2]}}code" '+
+                    '/></span>'+
+                    '<span><eg-marc-edit-editable '+
+                        'itype="sfv" '+
+                        'class="marcsfvalue" '+
+                        'field="field" '+
+                        'subfield="subfield" '+
+                        'content="subfield[1]" '+
+                        'on-keydown="onKeydown" '+
+                        'id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}s{{subfield[2]}}value" '+
+                    '/></span>'+
                   '</span>',
-        scope: { field: "=", subfield: "=" },
+        scope: { field: "=", subfield: "=", onKeydown: '=' },
         replace: false
     }
 })
 
 .directive("egMarcEditInd", function () {
     return {
+        transclude: true,
         restrict: 'E',
-        template: '<span><eg-marc-edit-editable type="ind" field="field" content="ind" max="1"/></span>',
-        scope: { ind : '=', field: '=' },
+        template: '<span><eg-marc-edit-editable itype="ind" field="field" content="ind" max="1" on-keydown="onKeydown" id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}i{{indNumber}}"/></span>',
+        scope: { ind : '=', field: '=', onKeydown: '=', indNumber: '@' },
         replace: false,
     }
 })
 
 .directive("egMarcEditTag", function () {
     return {
+        transclude: true,
         restrict: 'E',
-        template: '<span><eg-marc-edit-editable type="tag" field="field" content="tag" max="3"/></span>',
-        scope: { tag : '=', field: '=' },
+        template: '<span><eg-marc-edit-editable itype="tag" field="field" content="tag" max="3" on-keydown="onKeydown" id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}tag"/></span>',
+        scope: { tag : '=', field: '=', onKeydown: '=' },
         replace: false
     }
 })
 
 .directive("egMarcEditDatafield", function () {
     return {
+        transclude: true,
         restrict: 'E',
         template: '<div>'+
-                    '<span><eg-marc-edit-tag class="marctag" field="field" tag="field.tag"/></span>'+
-                    '<span><eg-marc-edit-ind class="marcind" field="field" ind="field.ind1"/></span>'+
-                    '<span><eg-marc-edit-ind class="marcind" field="field" ind="field.ind2"/></span>'+
-                    '<span><eg-marc-edit-subfield ng-repeat="subfield in field.subfields" subfield="subfield" field="field"/></span>'+
+                    '<span><eg-marc-edit-tag class="marctag" field="field" tag="field.tag" on-keydown="onKeydown"/></span>'+
+                    '<span><eg-marc-edit-ind class="marcind" field="field" ind="field.ind1" on-keydown="onKeydown" ind-number="1"/></span>'+
+                    '<span><eg-marc-edit-ind class="marcind" field="field" ind="field.ind2" on-keydown="onKeydown" ind-number="2"/></span>'+
+                    '<span><eg-marc-edit-subfield ng-repeat="subfield in field.subfields" subfield="subfield" field="field" on-keydown="onKeydown"/></span>'+
                   '</div>',
-        scope: { field: "=" }
+        scope: { field: "=", onKeydown: '=' }
     }
 })
 
 .directive("egMarcEditControlfield", function () {
     return {
+        transclude: true,
         restrict: 'E',
         template: '<div>'+
-                    '<span><eg-marc-edit-tag class="marctag" field="field" tag="field.tag"/></span>'+
-                    '<span><eg-marc-edit-editable type="cfld" field="field" class="marcdata" content="field.data"/></span>'+
+                    '<span><eg-marc-edit-tag class="marctag" field="field" tag="field.tag" on-keydown="onKeydown"/></span>'+
+                    '<span><eg-marc-edit-editable itype="cfld" field="field" class="marcdata" content="field.data" on-keydown="onKeydown" id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}data"/></span>'+
                   '</div>',
-        scope: { field: "=" }
+        scope: { field: "=", onKeydown: '=' }
     }
 })
 
 .directive("egMarcEditLeader", function () {
     return {
+        transclude: true,
         restrict: 'E',
         template: '<div>'+
-                    '<span><eg-marc-edit-editable class="marctag" content="tag"/></span>'+
-                    '<span><eg-marc-edit-editable class="marcdata" type="ldr" max="{{record.leader.length}}" content="record.leader"/></span>'+
+                    '<span><eg-marc-edit-editable class="marctag" content="tag" on-keydown="onKeydown" id="leadertag" disabled="disabled"/></span>'+
+                    '<span><eg-marc-edit-editable class="marcdata" itype="ldr" max="{{record.leader.length}}" content="record.leader" id="r{{record.subfield(\'901\',\'c\')[1]}}leaderdata" on-keydown="onKeydown"/></span>'+
                   '</div>',
         controller : ['$scope',
             function ( $scope ) {
                 $scope.tag = 'LDR';
             }
         ],
-        scope: { record: "=" }
+        scope: { record: "=", onKeydown: '=' }
     }
 })
 
@@ -195,31 +225,131 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
     return {
         template: '<form ng-submit="saveRecord()">'+
                   '<div class="marcrecord">'+
-                    '<div><eg-marc-edit-leader record="record"/></div>'+
-                    '<div><eg-marc-edit-controlfield ng-repeat="field in controlfields" field="field"/></div>'+
-                    '<div><eg-marc-edit-datafield ng-repeat="field in datafields" field="field"/></div>'+
+                    '<div><eg-marc-edit-leader record="record" on-keydown="onKeydown"/></div>'+
+                    '<div><eg-marc-edit-controlfield ng-repeat="field in controlfields" field="field" on-keydown="onKeydown"/></div>'+
+                    '<div><eg-marc-edit-datafield ng-repeat="field in datafields" field="field" on-keydown="onKeydown"/></div>'+
                   '</div>'+
                   '<button class="btn btn-default" type="submit">Save</button>'+
                   '</form>'+
                   '<button class="btn btn-default" ng-click="seeBreaker()">Breaker</button>',
         restrict: 'E',
         replace: false,
-        scope: { recordId : '=' },
-        controller : ['$scope','egCore',
-            function ( $scope , egCore ) {
+        scope: { recordId : '=', maxUndo : '@' },
+        controller : ['$timeout','$scope','egCore',
+            function ( $timeout , $scope , egCore ) {
+
+                $scope.max_undo = $scope.maxUndo || 100;
+                $scope.record_undo_stack = [];
+                $scope.record_redo_stack = [];
+                $scope.in_undo = false;
+                $scope.in_redo = false;
+                $scope.record = new MARC.Record();
+
+                $scope.onKeydown = function (event) {
+                    var event_return = true;
+
+                    if (event.which == 89 && event.ctrlKey) { // ctrl+y, redo
+                        event_return = $scope.processRedo();
+                    } else if (event.which == 90 && event.ctrlKey) { // ctrl+z, undo
+                        event_return = $scope.processUndo();
+                    } else { // Assumes only marc editor elements have IDs that can trigger this event handler.
+                        $scope.current_event_target = $(event.target).attr('id');
+                        if ($scope.current_event_target) {
+                            $scope.current_event_target_cursor_pos =
+                                event.target.selectionDirection=='backward' ?
+                                    event.target.selectionStart :
+                                    event.target.selectionEnd;
+                        }
+                    }
+
+                    return event_return;
+                };
+
+                function setCaret() {
+                    if ($scope.current_event_target) {
+                        var element = $('#'+$scope.current_event_target).get(0);
+                        element.focus();
+                        element.setSelectionRange(
+                            $scope.current_event_target_cursor_pos,
+                            $scope.current_event_target_cursor_pos
+                        );
+                        $scope.current_event_target = null;
+                    }
+                }
 
                 function loadRecord() {
                     return egCore.pcrud.retrieve(
                         'bre', $scope.recordId
                     ).then(function(rec) {
+                        $scope.in_redo = true;
                         $scope.bre = rec;
-                        $scope.record = new MARC.Record();
-                        $scope.record.fromXmlString( $scope.bre.marc() );
+                        $scope.record = new MARC.Record({ marcxml : $scope.bre.marc() });
                         $scope.controlfields = $scope.record.fields.filter(function(f){ return f.isControlfield() });
                         $scope.datafields = $scope.record.fields.filter(function(f){ return !f.isControlfield() });
-                    });
+                    }).then(setCaret);
                 }
 
+                $scope.$watch('record.toBreaker()', function (newVal, oldVal) {
+                    if (!$scope.in_undo && !$scope.in_redo && oldVal != newVal) {
+                        $scope.record_undo_stack.push({
+                            breaker: oldVal,
+                            target: $scope.current_event_target,
+                            pos: $scope.current_event_target_cursor_pos
+                        });
+                    }
+
+                    if ($scope.record_undo_stack.length > $scope.max_undo)
+                        $scope.record_undo_stack.shift();
+
+                    console.log('undo stack is ' + $scope.record_undo_stack.length + ' deep');
+                    $scope.in_redo = false;
+                    $scope.in_undo = false;
+                });
+
+                $scope.processUndo = function () {
+                    if ($scope.record_undo_stack.length) {
+                        $scope.in_undo = true;
+
+                        var undo_item = $scope.record_undo_stack.pop();
+                        $scope.record_redo_stack.push(undo_item);
+
+                        $scope.record = new MARC.Record({ marcbreaker : undo_item.breaker });
+                        $scope.controlfields = $scope.record.fields.filter(function(f){ return f.isControlfield() });
+                        $scope.datafields = $scope.record.fields.filter(function(f){ return !f.isControlfield() });
+
+                        $scope.current_event_target = undo_item.target;
+                        $scope.current_event_target_cursor_pos = undo_item.pos;
+                        console.log('Undo targeting ' + $scope.current_event_target + ' position ' + $scope.current_event_target_cursor_pos);
+
+                        $timeout(function(){$scope.$digest()}).then(setCaret);
+                        return false;
+                    }
+
+                    return true;
+                };
+
+                $scope.processRedo = function () {
+                    if ($scope.record_redo_stack.length) {
+                        $scope.in_redo = true;
+
+                        var redo_item = $scope.record_redo_stack.pop();
+                        $scope.record_undo_stack.push(redo_item);
+
+                        $scope.record = new MARC.Record({ marcbreaker : redo_item.breaker });
+                        $scope.controlfields = $scope.record.fields.filter(function(f){ return f.isControlfield() });
+                        $scope.datafields = $scope.record.fields.filter(function(f){ return !f.isControlfield() });
+
+                        $scope.current_event_target = redo_item.target;
+                        $scope.current_event_target_cursor_pos = redo_item.pos;
+                        console.log('Redo targeting ' + $scope.current_event_target + ' position ' + $scope.current_event_target_cursor_pos);
+
+                        $timeout(function(){$scope.$digest()}).then(setCaret);
+                        return false;
+                    }
+
+                    return true;
+                };
+
                 $scope.saveRecord = function () {
                     $scope.bre.marc($scope.record.toXmlString());
                     return egCore.pcrud.update(
index 876540a..22e5354 100644 (file)
@@ -116,13 +116,11 @@ var MARC = {
         // this.clone = function () { return dojo.clone(this) } // maybe implement later...
 
         this.fromXmlURL = function (url) {
-            this.ready   = false;
             var me = this;
             return $.get( // This is a Promise
                 url,
                 function (mxml) {
                     me.fromXmlDocument($('record', mxml)[0]);
-                    me.ready = true;
                     if (me.onLoad) me.onLoad();
             });
         },
@@ -135,18 +133,21 @@ var MARC = {
             var me = this;
             me.leader = $($('leader',mxml)[0]).text() || '00000cam a2200205Ka 4500';
 
-            $('controlfield', mxml).each(function () {
+            $('controlfield', mxml).each(function (ind) {
                 var cf=$(this);
                 me.fields.push(
                     new MARC.Field({
                           record : me,
                           tag    : cf.attr('tag'),
-                          data   : cf.text()
+                          data   : cf.text(),
+                          position: ind
                     })
                 )
             });
 
-            $('datafield', mxml).each(function () {
+            var cfield_count = me.fields.length + 1;
+
+            $('datafield', mxml).each(function (ind) {
                 var df=$(this);
                 me.fields.push(
                     new MARC.Field({
@@ -154,14 +155,16 @@ var MARC = {
                         tag       : df.attr('tag'),
                         ind1      : df.attr('ind1'),
                         ind2      : df.attr('ind2'),
+                        position  : ind + cfield_count,
                         subfields : $('subfield', df).map(
                             function (i, sf) {
-                                return [[ $(sf).attr('code'), $(sf).text() ]];
+                                return [[ $(sf).attr('code'), $(sf).text(), i ]];
                             }
                         ).get()
                     })
                 )
             });
+            me.ready = true;
 
         },
 
@@ -216,7 +219,7 @@ var MARC = {
             }
             
             var lines = marctxt.replace(/^=/gm,'').split('\n');
-            lines.forEach(function (current_line) {
+            lines.forEach(function (current_line, ind) {
 
                 if (current_line.match(/^#/)) {
                     // skip comment lines
@@ -228,7 +231,8 @@ var MARC = {
                             new MARC.Field({
                                 record : me,
                                 tag    : line_tag(current_line),
-                                data   : cf_line_data(current_line).replace('\\',' ','g')
+                                data   : cf_line_data(current_line).replace('\\',' ','g'),
+                                position: ind
                             })
                         );
                     }
@@ -248,16 +252,18 @@ var MARC = {
                                 tag       : line_tag(current_line),
                                 ind1      : df_ind1(current_line),
                                 ind2      : df_ind2(current_line),
-                                subfields : sf_list.map( function (sf) {
+                                position  : ind,
+                                subfields : sf_list.map( function (sf, i) {
                                                 var sf_data = sf.substring(1);
                                                 if (me.delimiter == '$') sf_data = sf_data.replace(/\{dollar\}/g, '$');
-                                                return [ sf.substring(0,1), sf_data ];
+                                                return [ sf.substring(0,1), sf_data, i ];
                                             })
                         })
                     );
                 }
             });
 
+            me.ready = true;
             return this;
         },
 
@@ -446,6 +452,7 @@ var MARC = {
             return val;
         }
 
+        this.ready = false;
         this.fields = [];
         this.delimiter = '\u2021';
         this.leader = '00000cam a2200205Ka 4500';
@@ -571,6 +578,7 @@ var MARC = {
         this.data = ''; // MARC data for a controlfield element
         this.subfields = []; // list of MARC subfields for a datafield element
 
+        this.position = kwargs.position;
         this.record = kwargs.record;
         this.tag = kwargs.tag;
         this.ind1 = kwargs.ind1 || ' ';