LPXXX text undos
authorBill Erickson <berickxx@gmail.com>
Mon, 2 Dec 2019 16:01:39 +0000 (11:01 -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.ts
Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts

index ac7f5eb..33209e6 100644 (file)
@@ -1,8 +1,10 @@
-import {ElementRef, Component, Input, Output, OnInit, EventEmitter, 
-    AfterViewInit, Renderer2} from '@angular/core';
+import {ElementRef, Component, Input, Output, OnInit, OnDestroy, 
+    EventEmitter, AfterViewInit, Renderer2} from '@angular/core';
+import {Subscription} from 'rxjs';
 import {filter} from 'rxjs/operators';
 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
-import {MarcEditContext, FieldFocusRequest, MARC_EDITABLE_FIELD_TYPE} from './editor-context';
+import {MarcEditContext, FieldFocusRequest, MARC_EDITABLE_FIELD_TYPE, 
+    UndoRedoAction} from './editor-context';
 import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
 import {TagTableService} from './tagtable.service';
 
@@ -16,7 +18,8 @@ import {TagTableService} from './tagtable.service';
   styleUrls: ['./editable-content.component.css']
 })
 
-export class EditableContentComponent implements OnInit, AfterViewInit {
+export class EditableContentComponent 
+    implements OnInit, AfterViewInit, OnDestroy {
 
     @Input() context: MarcEditContext;
     @Input() field: MarcField;
@@ -37,6 +40,11 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
     randId = Math.floor(Math.random() * 100000);
     editInput: any; // <input/> or <div contenteditable/>
     maxLength: number = null;
+    initialContent: string; // TODO set me on blur as well
+
+    focusSub: Subscription;
+    undoSub: Subscription;
+    redoSub: Subscription;
 
     constructor(
         private renderer: Renderer2,
@@ -46,26 +54,43 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
         this.setupFieldType();
     }
 
+    ngOnDestroy() {
+        if (this.focusSub) { this.focusSub.unsubscribe(); }
+        if (this.undoSub) { this.undoSub.unsubscribe(); }
+        if (this.redoSub) { this.redoSub.unsubscribe(); }
+    }
+
     watchForFocusRequests() {
+        this.focusSub = this.context.fieldFocusRequest.pipe(
+            filter((req: FieldFocusRequest) => this.focusRequestIsMe(req)))
+        .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));
+    }
+
+    watchForRedoRequests() {
+        this.redoSub = this.context.redoRequest.pipe(
+            filter((redo: UndoRedoAction) => this.focusRequestIsMe(redo.position)))
+        .subscribe((redo: UndoRedoAction) => this.processRedo(redo));
+    }
 
-        if (!this.field) {
-            // LDR tag has no field associated
-            return;
-        }
 
-        this.context.fieldFocusRequest.pipe(
-            filter(req => req.fieldId === this.field.fieldId),
-            filter(req => req.target  === this.fieldType)
-        ).subscribe((req: FieldFocusRequest) => {
+    focusRequestIsMe(req: FieldFocusRequest): boolean {
+        if (!this.field) { return false; } // LDR
+        if (req.fieldId !== this.field.fieldId) { return false; }
+        if (req.target !== this.fieldType) { return false; }
 
-            if (req.sfOffset !== undefined && 
-                req.sfOffset !== this.subfield[2]) {
-                // this is not the subfield you are looking for.
-                return;
-            }
+        if (req.sfOffset !== undefined && 
+            req.sfOffset !== this.subfield[2]) {
+            // this is not the subfield you are looking for.
+            return false;
+        }
 
-            this.selectText(req);
-        });
+        return true;
     }
 
     selectText(req?: FieldFocusRequest) {
@@ -78,7 +103,7 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
         if (!req) {
             // Focus request may have come from keyboard navigation, 
             // clicking, etc.  Model the event as a focus request
-            // so our context can track it just like an auto-focus.
+            // so it can be tracked the same.
             req = {
                 fieldId: this.field.fieldId, 
                 target: this.fieldType,
@@ -91,6 +116,7 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
 
     setupFieldType() {
         const content = this.getContent();
+        this.initialContent = content;
 
         switch (this.fieldType) {
             case 'ldr':
@@ -100,21 +126,30 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
             case 'tag':
                 this.maxLength = 3;
                 this.watchForFocusRequests();
+                this.watchForUndoRequests();
+                this.watchForRedoRequests();
                 break;
 
             case 'ind1':
             case 'ind2':
                 this.maxLength = 1;
+                this.watchForFocusRequests();
+                this.watchForUndoRequests();
+                this.watchForRedoRequests();
                 break;
 
             case 'sfc':
                 this.maxLength = 1;
                 this.watchForFocusRequests();
+                this.watchForUndoRequests();
+                this.watchForRedoRequests();
                 break;
 
             case 'sfv':
                 this.bigText = true;
                 this.watchForFocusRequests();
+                this.watchForUndoRequests();
+                this.watchForRedoRequests();
                 break;
         }
     }
@@ -158,7 +193,7 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
         }
     }
 
-    setContent(value: string) {
+    setContent(value: string, propagatBigText?: boolean, skipUndoTrack?: boolean) {
 
         if (this.fieldText) { return; } // read-only text
 
@@ -171,6 +206,53 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
             case 'ind1': // thunk
             case 'ind2': this.field[this.fieldType] = value; break;
         }
+
+        if (propagatBigText && this.bigText) {
+            // Propagate new content to the bigtext div.
+            // Should only be used when a content change occurrs via
+            // external means (i.e. not from a direct edit of the div).
+            this.editInput.innerText = value;
+        }
+
+        if (!skipUndoTrack) {
+            this.trackTextChangeForUndo(value);
+        }
+    }
+
+    trackTextChangeForUndo(value: string) {
+
+        const lastUndo = this.context.undoStack[0];
+
+        if (lastUndo
+            && lastUndo.textContent !== undefined
+            && this.focusRequestIsMe(lastUndo.position)) {
+            // Most recent undo entry was a text change event for us.
+            // Nothing else to track.
+            return;
+        }
+
+        // Track the initial content since that's what we would undo to.
+        const undo = {
+            textContent: this.initialContent,
+            position: this.context.lastFocused
+        };
+
+        this.context.undoStack.unshift(undo);
+    }
+
+    processUndo(undo: UndoRedoAction) {
+
+        if (undo.textContent !== undefined) {
+            // Undoing a text change
+            const redoContent = this.getContent();
+            this.setContent(undo.textContent, true, true);
+
+            undo.textContent = redoContent;
+            this.context.redoStack.unshift(undo);
+        }
+    }
+
+    processRedo(undo: UndoRedoAction) {
     }
 
     // Propagate editable div content into our record
@@ -311,6 +393,13 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
                 }
                 break;
 
+            case 'z': 
+                if (evt.ctrlKey) { // undo
+                    this.context.requestUndo();
+                    evt.preventDefault();
+                }
+                break;
+
         }
     }
 
@@ -355,11 +444,7 @@ export class EditableContentComponent implements OnInit, AfterViewInit {
     }
 
     contextMenuChange(value: string) {
-        this.setContent(value);
-        if (this.bigText) {
-            // propagate to div
-            this.editInput.innerText = value;
-        }
+        this.setContent(value, true);
     }
 }
 
index 11ce5fb..c99f934 100644 (file)
@@ -21,14 +21,16 @@ export interface FieldFocusRequest {
 }
 
 export interface UndoRedoAction {
-    breakerText: string;
     position: FieldFocusRequest;
+    textContent?: string;
 }
 
 export class MarcEditContext {
 
     recordChange: EventEmitter<MarcRecord>;
     fieldFocusRequest: EventEmitter<FieldFocusRequest>;
+    undoRequest: EventEmitter<UndoRedoAction>;
+    redoRequest: EventEmitter<UndoRedoAction>;
     recordType: 'biblio' | 'authority' = 'biblio';
 
     lastFocused: FieldFocusRequest = null;
@@ -52,6 +54,8 @@ export class MarcEditContext {
     constructor() {
         this.recordChange = new EventEmitter<MarcRecord>();
         this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
+        this.undoRequest = new EventEmitter<UndoRedoAction>();
+        this.redoRequest = new EventEmitter<UndoRedoAction>();
     }
 
     requestFieldFocus(req: FieldFocusRequest) {
@@ -62,6 +66,17 @@ 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);
+        }
+    }
+
     getFieldOffset(fieldId: number): number {
         for (let idx = 0; idx < this.record.fields.length; idx++) {
             if (this.record.fields[idx].fieldId === fieldId) {
@@ -75,7 +90,6 @@ export class MarcEditContext {
             this.getFieldOffset(this.lastFocused.fieldId);
 
         this.undoStack.unshift({
-            breakerText: this.record.toBreaker(), 
             position: this.lastFocused
         });
     }