LP#1691263: make webstaff MARC editor wrap long fields
authorCesar Velez <cesar.velez@equinoxinitiative.org>
Mon, 4 Dec 2017 17:45:35 +0000 (12:45 -0500)
committerKathy Lussier <klussier@masslnc.org>
Wed, 14 Mar 2018 20:10:24 +0000 (16:10 -0400)
This patch makes the MARC editor wrap long fields (e.g.,
bibliographic 505 fields) so that they fit the width of the enclosing
window or modal. The approach taken is replacing the text input
elements with contenteditable divs, which in turn can be better
styled.

To test
-------
[1] Apply the patch.
[2] Locate a record with a long 505 field and open it in the
    MARC editor. Verify that the contents of the field wrap.
[3] Verify that record editing and saving work as expected.

Signed-off-by: Cesar Velez <cesar.velez@equinoxinitiative.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>

Open-ILS/src/templates/staff/cat/share/t_marcedit_editable.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit_editable.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit_editable.tt2
new file mode 100644 (file)
index 0000000..1e5888f
--- /dev/null
@@ -0,0 +1,20 @@
+<span style="all:unset;">
+<input
+  ng-show="itype != 'sfv'"
+  ng-disabled="{{isInputDisabled}}"
+  ng-class="['marcedit', {'marcsfcode': itype == 'sfc','marcind': itype == 'ind' || itype == 'tag', 'focusable': itype != 'sfv'}]"
+  style="font-family: 'Lucida Console', Monaco, monospace; min-width: 1ch; margin: 0 -2px;"
+  ng-model="content"
+  size="{{content.length * 1.1}}"
+  maxlength="{{max}}"
+  class=""
+  type="text">
+</input>
+<div contenteditable
+  class=""
+  ng-class="['marcedit', {'marcsfvalue': itype == 'sfv', 'focusable': itype == 'sfv'}]"
+  ng-show="itype == 'sfv'"
+  style="font-family: 'Lucida Console', Monaco, monospace; display: inline-block; min-width: 1ch; margin: 0 -1px; padding: 0;"
+  ng-model="content"
+>{{content}}</div>
+</span>
index 2430dc0..4411098 100644 (file)
@@ -44,18 +44,34 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
     }
 }])
 
+.directive("contenteditable", function() {
+    return {
+        restrict: "A",
+        require: "ngModel",
+        link: function(scope,element,attrs,ngModel){
+
+            function read(){
+                // save new text into model
+                var elhtml = element.text();
+                ngModel.$setViewValue(elhtml);
+            }
+
+            ngModel.$render = function(){
+                element.text(ngModel.$viewValue || "");
+            };
+
+            element.bind("blur.c_e keyup.c_e change.c_e", function(){
+                scope.$apply(read);
+            });
+        }
+    };
+})
+
 .directive("egMarcEditEditable", ['$timeout', '$compile', '$document', function ($timeout, $compile, $document) {
     return {
         restrict: 'E',
         replace: true,
-        template: '<input '+
-                      'style="font-family: \'Lucida Console\', Monaco, monospace;" '+
-                      'ng-model="content" '+
-                      'size="{{content.length * 1.1}}" '+
-                      'maxlength="{{max}}" '+
-                      'class="" '+
-                      'type="text" '+
-                  '/>',
+        templateUrl: './cat/share/t_marcedit_editable',
         scope: {
             field: '=',
             onKeydown: '=',
@@ -66,11 +82,12 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
             max: '@',
             itype: '@',
             selectOnFocus: '=',
-            advanceFocusAfterInput: '='
+            advanceFocusAfterInput: '=',
+            isDisabled: "="
         },
         controller : ['$scope',
             function ( $scope ) {
-
+                $scope.isInputDisabled = $scope.isDisabled == 'disabled';
                 if ($scope.contextItemContainer && angular.isArray($scope.$parent[$scope.contextItemContainer]))
                     $scope.item_container = $scope.$parent[$scope.contextItemContainer];
                 else if ($scope.contextItemGenerator)
@@ -146,9 +163,24 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
             if (Boolean(scope.selectOnFocus)) {
                 element.addClass('noSelection');
-                element.bind('focus', function () { element.select() });
+                element.bind('focus', function (e) {
+                    var el = $(e.target).children('input').first();
+                    if (el.select) { el.select(); }
+                });
             }
 
+            element.children("div[contenteditable]").each(function() {
+                $(this).focus(function(e) {
+                    var tNode = e.target.firstChild;
+                    var range = document.createRange();
+                    range.setStart(tNode, 0);
+                    range.setEnd(tNode, tNode.length);
+                    var sel = window.getSelection();
+                    sel.removeAllRanges();
+                    sel.addRange(range);
+                });
+            });
+
             function findCaretTarget(id, itype) {
                 var tgt = null;
                 if (itype == 'tag') {
@@ -349,6 +381,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     '/></span>'+
                     '<span><eg-marc-edit-editable '+
                         'itype="sfv" '+
+                        'select-on-focus="true" '+
                         'class="marcedit marcsf marcsfvalue" '+
                         'field="field" '+
                         'subfield="subfield" '+
@@ -590,7 +623,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                       'content="tag" '+
                       'on-keydown="onKeydown" '+
                       'id="leadertag" '+
-                      'disabled="disabled"'+
+                      'is-disabled="disabled"'+
                       '/></span>'+
                     '<span><eg-marc-edit-editable '+
                       'class="marcedit marcdata" '+
@@ -858,12 +891,19 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             index_sf = event.data.scope.subfield[2];
                             new_sf = index_sf + 1;
 
-                            var start = event.target.selectionStart;
-                            var end = event.target.selectionEnd - event.target.selectionStart ?
-                                    event.target.selectionEnd :
-                                    event.target.value.length;
+                            var start = event.target.selectionStart || getCaretPosEditableDiv(element);
+                            var end;
+                            if (event.target.value){
+                                end = event.target.selectionEnd - event.target.selectionStart ?
+                                        event.target.selectionEnd :
+                                        event.target.value.length;
+                            } else {
+                                end = element.text().length;
+                            }
 
-                            move_data = event.target.value.substring(start,end);
+                            move_data = element.value ?
+                                element.value.substring(start,end) :
+                                element.text().substring(start, end);
 
                         } else if (element.hasClass('marcsfcode')) {
                             index_sf = event.data.scope.subfield[2];
@@ -879,7 +919,11 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
                         event.data.scope.field.subfields.forEach(function(sf) {
                             if (sf[2] >= new_sf) sf[2]++;
-                            if (sf[2] == index_sf) sf[1] = event.target.value.substring(0,start) + event.target.value.substring(end);
+                            if (sf[2] == index_sf) {
+                                sf[1] = event.target.value ?
+                                    event.target.value.substring(0,start) + event.target.value.substring(end) :
+                                    element.text().substring(0, start);
+                            }
                         });
                         event.data.scope.field.subfields.splice(
                             new_sf,
@@ -914,7 +958,8 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                         deleteDatafield(event);
                         event_return = false;
 
-                    } else if (event.which == 46 && event.shiftKey && $(event.target).hasClass('marcsf')) { // shift+del, remove subfield
+                    } else if (event.which == 46 && event.shiftKey && ($(event.target).hasClass('marcsf') || $(event.target.parentNode).hasClass('marcsf'))) { 
+                        // shift+del, remove subfield
 
                         var sf = event.data.scope.subfield[2] - 1;
                         if (sf == -1) sf = 0;
@@ -1047,7 +1092,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                         event_return = false;
 
                     } else { // Assumes only marc editor elements have IDs that can trigger this event handler.
-                        $scope.current_event_target = $(event.target).attr('id');
+                        $scope.current_event_target = $(event.target).hasClass('focusable') ? $(event.target) : null;//.attr('id');
                         if ($scope.current_event_target) {
                             $scope.current_event_target_cursor_pos =
                                 event.target.selectionDirection=='backward' ?
@@ -1065,7 +1110,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                         if (!$scope.current_event_target_cursor_pos_end)
                             $scope.current_event_target_cursor_pos_end = $scope.current_event_target_cursor_pos
 
-                        var element = $('#'+$scope.current_event_target).get(0);
+                        var element = $('#'+$scope.current_event_target + " .focusable").get(0);
                         if (element) {
                             element.focus();
                             if (element.setSelectionRange) {
@@ -1074,10 +1119,24 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                                     $scope.current_event_target_cursor_pos_end
                                 );
                             }
-                            $scope.current_event_cursor_pos_end = null;
-                            $scope.current_event_target = null;
+                        }
+                        $scope.current_event_cursor_pos_end = null;
+                        $scope.current_event_target = null;
+                    }
+                }
+
+                function getCaretPosEditableDiv(editableDiv){
+                    var caretPos = 0, sel, range;
+                    if (window.getSelection) {
+                        sel = window.getSelection();
+                        if (sel.rangeCount) {
+                            range = sel.getRangeAt(0);
+                            if (range.commonAncestorContainer.parentNode == editableDiv[0]) {
+                                caretPos = range.endOffset;
+                            }
                         }
                     }
+                    return caretPos;
                 }
 
                 function loadRecord() {