LPXXX undo/redo
authorBill Erickson <berickxx@gmail.com>
Tue, 3 Dec 2019 23:14:34 +0000 (18:14 -0500)
committerBill Erickson <berickxx@gmail.com>
Fri, 6 Dec 2019 15:37:03 +0000 (10:37 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.html
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts

index 25289e1..359128c 100644 (file)
@@ -9,6 +9,7 @@
     (menuItemSelected)="contextMenuChange($event.value)"
     (keydown)="inputKeyDown($event)"
     (focus)="selectText()"
+    (blur)="inputBlurred()"
     (input)="bigTextValueChange()">
   </div>
 </ng-container>
@@ -26,6 +27,7 @@
     (menuItemSelected)="contextMenuChange($event.value)"
     (keydown)="inputKeyDown($event)"
     (focus)="selectText()"
+    (blur)="inputBlurred()"
     [ngModel]="getContent()"
     (ngModelChange)="setContent($event)"
   />
index 9b2c98b..0005a78 100644 (file)
@@ -43,11 +43,10 @@ export class EditableContentComponent
 
     // Track the load-time content so we know what text value to 
     // track on our undo stack.
-    undoBackToContent: string;
+    undoBackToText: string;
 
     focusSub: Subscription;
-    undoSub: Subscription;
-    redoSub: Subscription;
+    undoRedoSub: Subscription;
 
     constructor(
         private renderer: Renderer2,
@@ -59,8 +58,7 @@ export class EditableContentComponent
 
     ngOnDestroy() {
         if (this.focusSub) { this.focusSub.unsubscribe(); }
-        if (this.undoSub) { this.undoSub.unsubscribe(); }
-        if (this.redoSub) { this.redoSub.unsubscribe(); }
+        if (this.undoRedoSub) { this.undoRedoSub.unsubscribe(); }
     }
 
     watchForFocusRequests() {
@@ -69,19 +67,12 @@ export class EditableContentComponent
         .subscribe((req: FieldFocusRequest) => this.selectText(req));
     }
 
-    watchForUndoRequests() {
-        this.undoSub = this.context.undoRequest.pipe(
-            filter((undo: UndoRedoAction) => this.focusRequestIsMe(undo.position)))
-        .subscribe((undo: UndoRedoAction) => this.processUndo(undo));
+    watchForUndoRedoRequests() {
+        this.undoRedoSub = this.context.undoRedoRequest.pipe(
+            filter((action: UndoRedoAction) => this.focusRequestIsMe(action.position)))
+        .subscribe((action: UndoRedoAction) => this.processUndoRedo(action));
     }
 
-    watchForRedoRequests() {
-        this.redoSub = this.context.redoRequest.pipe(
-            filter((redo: UndoRedoAction) => this.focusRequestIsMe(redo.position)))
-        .subscribe((redo: UndoRedoAction) => this.processRedo(redo));
-    }
-
-
     focusRequestIsMe(req: FieldFocusRequest): boolean {
         if (!this.field) { return false; } // LDR
         if (req.fieldId !== this.field.fieldId) { return false; }
@@ -110,7 +101,7 @@ export class EditableContentComponent
             req = {
                 fieldId: this.field.fieldId, 
                 target: this.fieldType,
-                sfOffset: this.subfield ? this.subfield[2] : null
+                sfOffset: this.subfield ? this.subfield[2] : undefined
             };
         }
 
@@ -119,7 +110,7 @@ export class EditableContentComponent
 
     setupFieldType() {
         const content = this.getContent();
-        this.undoBackToContent = content;
+        this.undoBackToText = content;
 
         switch (this.fieldType) {
             case 'ldr':
@@ -129,30 +120,31 @@ export class EditableContentComponent
             case 'tag':
                 this.maxLength = 3;
                 this.watchForFocusRequests();
-                this.watchForUndoRequests();
-                this.watchForRedoRequests();
+                this.watchForUndoRedoRequests();
+                break;
+
+            case 'cfld':
+                this.watchForFocusRequests();
+                this.watchForUndoRedoRequests();
                 break;
 
             case 'ind1':
             case 'ind2':
                 this.maxLength = 1;
                 this.watchForFocusRequests();
-                this.watchForUndoRequests();
-                this.watchForRedoRequests();
+                this.watchForUndoRedoRequests();
                 break;
 
             case 'sfc':
                 this.maxLength = 1;
                 this.watchForFocusRequests();
-                this.watchForUndoRequests();
-                this.watchForRedoRequests();
+                this.watchForUndoRedoRequests();
                 break;
 
             case 'sfv':
                 this.bigText = true;
                 this.watchForFocusRequests();
-                this.watchForUndoRequests();
-                this.watchForRedoRequests();
+                this.watchForUndoRedoRequests();
                 break;
         }
     }
@@ -228,44 +220,42 @@ export class EditableContentComponent
 
         if (lastUndo
             && lastUndo.textContent !== undefined
+            && lastUndo.textContent === this.undoBackToText
             && this.focusRequestIsMe(lastUndo.position)) {
-            // Most recent undo entry was a text change event for us.
+            // Most recent undo entry was a text change event within the
+            // current atomic editing (focused) session for the input.
             // Nothing else to track.
             return;
         }
 
         const undo = {
-            textContent: this.undoBackToContent,
+            textContent: this.undoBackToText,
             position: this.context.lastFocused
         };
 
         this.context.undoStack.unshift(undo);
     }
 
-    // Apply the undo action and track it as a redo
-    processUndo(undo: UndoRedoAction) {
+    // Apply the undo or redo action and track its opposite
+    // action on the necessary stack
+    processUndoRedo(action: UndoRedoAction) {
 
-        if (undo.textContent !== undefined) {
-            // Undoing a text change
-            const redoContent = this.getContent();
-            this.setContent(undo.textContent, true, true);
+        // Undoing a text change
+        const recoverContent = this.getContent();
+        this.setContent(action.textContent, true, true);
 
-            undo.textContent = redoContent;
-            this.context.redoStack.unshift(undo);
-        }
-    }
-
-    // Apply the redo action and track it as an undo
-    processRedo(redo: UndoRedoAction) {
+        action.textContent = recoverContent;
+        const moveTo = action.isRedo ? 
+            this.context.undoStack : this.context.redoStack;
 
-        if (redo.textContent !== undefined) {
-            // redoing a text change
-            const undoContent = this.getContent();
-            this.setContent(redo.textContent, true, true);
+        moveTo.unshift(action);
+    }
 
-            redo.textContent = undoContent;
-            this.context.undoStack.unshift(redo);
-        }
+    inputBlurred() {
+        // If the text content changed during this focus session,
+        // track the new value as the value the next session of 
+        // text edits should return to upon undo.
+        this.undoBackToText = this.getContent();
     }
 
     // Propagate editable div content into our record
@@ -372,7 +362,7 @@ export class EditableContentComponent
             case 'F6': 
                 if (evt.shiftKey) {
                     // shift+F6 => add 006
-                    this.context.add006();
+                    this.context.add00X('006');
                     evt.preventDefault();
                     evt.stopPropagation();
                 }
@@ -381,7 +371,7 @@ export class EditableContentComponent
             case 'F7': 
                 if (evt.shiftKey) {
                     // shift+F7 => add 007
-                    this.context.add007();
+                    this.context.add00X('007');
                     evt.preventDefault();
                     evt.stopPropagation();
                 }
index f4f6c12..90448cb 100644 (file)
@@ -10,27 +10,36 @@ export type MARC_EDITABLE_FIELD_TYPE =
 export interface FieldFocusRequest {
     fieldId: number;
     target: MARC_EDITABLE_FIELD_TYPE;
-
-    // in some cases, field IDs change out from under our feet (e.g.
-    // undo / redo applications) so track the tag offset as well
-    // so we can find our way back if needed.
-    tagOffset?: number;
-
-    // focus a specific subfield by its offset
-    sfOffset?: number; 
+    sfOffset?: number; // focus a specific subfield by its offset
 }
 
 export interface UndoRedoAction {
+
+    // Which point in the record was modified.
     position: FieldFocusRequest;
+
+    isRedo?: boolean;
+
+    // Track text changes.
     textContent?: string;
+
+    // Retain a copy of the affected field so deletion
+    // recovery can extract what's needed.
+    field?: MarcField;
+
+    // Does this action track an addition or deletion.
+    wasAddition?: boolean;
+
+    // Reference to position just before the modified position
+    // in the record, so deletion recovery can correctly position.
+    precedingPosition?: FieldFocusRequest;
 }
 
 export class MarcEditContext {
 
     recordChange: EventEmitter<MarcRecord>;
     fieldFocusRequest: EventEmitter<FieldFocusRequest>;
-    undoRequest: EventEmitter<UndoRedoAction>;
-    redoRequest: EventEmitter<UndoRedoAction>;
+    undoRedoRequest: EventEmitter<UndoRedoAction>;
     recordType: 'biblio' | 'authority' = 'biblio';
 
     lastFocused: FieldFocusRequest = null;
@@ -54,8 +63,7 @@ export class MarcEditContext {
     constructor() {
         this.recordChange = new EventEmitter<MarcRecord>();
         this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
-        this.undoRequest = new EventEmitter<UndoRedoAction>();
-        this.redoRequest = new EventEmitter<UndoRedoAction>();
+        this.undoRedoRequest = new EventEmitter<UndoRedoAction>();
     }
 
     requestFieldFocus(req: FieldFocusRequest) {
@@ -66,65 +74,91 @@ export class MarcEditContext {
         });
     }
 
-    // Broadcast the next undo action on the stack.  The handler of the
-    // undo is responsible for creating the analogous redo request and
-    // adding it to our redo stack.
     requestUndo() {
         const undo = this.undoStack.shift();
         if (undo) {
-            this.undoRequest.emit(undo);
-            this.requestFieldFocus(undo.position);
+            undo.isRedo = false;
+            this.distributeUndoRedo(undo);
         }
     }
 
     requestRedo() {
         const redo = this.redoStack.shift();
         if (redo) {
-            this.redoRequest.emit(redo);
-            this.requestFieldFocus(redo.position);
+            redo.isRedo = true;
+            this.distributeUndoRedo(redo);
         }
     }
 
-    getFieldOffset(fieldId: number): number {
-        for (let idx = 0; idx < this.record.fields.length; idx++) {
-            if (this.record.fields[idx].fieldId === fieldId) {
-                return idx;
-            }
+    distributeUndoRedo(action: UndoRedoAction) {
+        if (action.textContent !== undefined) {
+            // Let the editable content component handle it.
+            this.undoRedoRequest.emit(action);
+        } else {
+            // Manage structural changes within
+            this.handleStructuralUndoRedo(action);
         }
     }
 
-    recordChanging() {
-        this.lastFocused.tagOffset = 
-            this.getFieldOffset(this.lastFocused.fieldId);
+    handleStructuralUndoRedo(action: UndoRedoAction, isRedo?: boolean) {
 
-        this.undoStack.unshift({
-            position: this.lastFocused
-        });
-    }
+        if (action.wasAddition) {
+            this.record.deleteFields(action.field);
+            this.requestFieldFocus(action.precedingPosition);
 
-    add006() {
-        this.recordChanging();
-        this.record.insertOrderedFields(
-            this.record.newField({
-                tag : '006',
-                data : '                                        '
-            })
-        );
+        } else {
+            const fieldId = action.field.fieldId;
+            const prevField = 
+                this.record.getField(action.precedingPosition.fieldId);
+
+            this.record.insertFieldsAfter(prevField, action.field);
+            
+            // Recover the original fieldId, which gets re-stamped
+            // in this.record.insertFields* calls.
+            action.field.fieldId = fieldId;
+            
+            // Focus the newly recovered field.
+            this.requestFieldFocus(action.position);
+        }
+
+        action.wasAddition = !action.wasAddition;
+
+        const moveTo = isRedo ? this.undoStack : this.redoStack;
+
+        moveTo.unshift(action);
     }
 
+    add00X(tag: string) {
 
-    add007() {
-        this.recordChanging();
-        this.record.insertOrderedFields(
-            this.record.newField({
-                tag : '007',
-                data : '                                        '
-            })
-        );
+        const field: MarcField = this.record.newField({
+            tag : tag,
+            data : '                                        '
+        });
+
+        this.record.insertOrderedFields(field);
+
+        const focus: FieldFocusRequest = 
+            {fieldId: field.fieldId, target: 'tag'};
+
+        const prevField = this.record.getPreviousField(field.fieldId);
+
+        let prevFocus: FieldFocusRequest;
+        if (prevField) {
+            prevFocus = {fieldId: prevField.fieldId, target: 'tag'};
+        }
+
+        this.undoStack.unshift({
+            wasAddition: true,
+            field: field,
+            position: focus,
+            precedingPosition: prevFocus
+        });
+
+        this.requestFieldFocus(focus);
     }
 
     insertReplace008() {
-        this.recordChanging();
+        //this.recordChanging();
 
         // delete all of the 008s
         [].concat(this.record.field('008', true)).forEach(
@@ -142,7 +176,7 @@ export class MarcEditContext {
     // Adds a new empty subfield to the provided field at the
     // requested subfield position
     insertSubfield(field: MarcField, position: number) {
-        this.recordChanging();
+        //this.recordChanging();
 
         // array index 3 contains that position of the subfield
         // in the MARC field.  When splicing a new subfield into
@@ -171,7 +205,7 @@ export class MarcEditContext {
     }
 
     insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
-        this.recordChanging();
+        //this.recordChanging();
 
         if (before) {
             this.record.insertFieldsBefore(contextField, newField);
@@ -185,13 +219,13 @@ export class MarcEditContext {
 
 
     deleteField(field: MarcField) {
-        this.recordChanging();
+        //this.recordChanging();
         this.record.deleteFields(field);
         this.focusNextTag(field) || this.focusPreviousTag(field);
     }
 
     deleteSubfield(field: MarcField, subfield: MarcSubfield) {
-        this.recordChanging();
+        //this.recordChanging();
         // If subfields remain, focus the previous subfield.
         // otherwise focus our tag.
         const sfpos = subfield[2] - 1;