LP1852782 MARC editor Physical Characteristics Wizard
[Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / marc-edit / editor-context.ts
index 168fbda..49ed524 100644 (file)
@@ -1,12 +1,13 @@
 import {EventEmitter} from '@angular/core';
 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+import {TagTable} from './tagtable.service';
 
 /* Per-instance MARC editor context. */
 
 const STUB_DATA_00X = '                                        ';
 
-export type MARC_EDITABLE_FIELD_TYPE = 
+export type MARC_EDITABLE_FIELD_TYPE =
     'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
 
 export interface FieldFocusRequest {
@@ -14,6 +15,11 @@ export interface FieldFocusRequest {
     target: MARC_EDITABLE_FIELD_TYPE;
     sfOffset?: number; // focus a specific subfield by its offset
     ffCode?: string; // fixed field code
+
+    // If set, an external source wants to modify the text content
+    // of an editable component (in a way that retains undo/redo
+    // functionality).
+    newText?: string;
 }
 
 export class UndoRedoAction {
@@ -22,6 +28,10 @@ export class UndoRedoAction {
 
     // Which stack do we toss this on once it's been applied?
     isRedo: boolean;
+
+    // Grouped actions are tracked as multiple undo / redo actions, but
+    // are done and un-done as a unit.
+    groupSize?: number;
 }
 
 export class TextUndoRedoAction extends UndoRedoAction {
@@ -61,6 +71,13 @@ export class MarcEditContext {
     undoStack: UndoRedoAction[] = [];
     redoStack: UndoRedoAction[] = [];
 
+    tagTable: TagTable;
+
+    // True if any changes have been made.
+    // For the 'rich' editor, this is any un-do-able actions.
+    // For the text edtior it's any text change.
+    changesPending: boolean;
+
     private _record: MarcRecord;
     set record(r: MarcRecord) {
         if (r !== this._record) {
@@ -83,7 +100,9 @@ export class MarcEditContext {
     requestFieldFocus(req: FieldFocusRequest) {
         // timeout allows for new components to be built before the
         // focus request is emitted.
-        setTimeout(() => this.fieldFocusRequest.emit(req));
+        if (req) {
+            setTimeout(() => this.fieldFocusRequest.emit(req));
+        }
     }
 
     resetUndos() {
@@ -92,18 +111,75 @@ export class MarcEditContext {
     }
 
     requestUndo() {
-        const undo = this.undoStack.shift();
-        if (undo) {
-            undo.isRedo = false;
-            this.distributeUndoRedo(undo);
-        }
+        let remaining = null;
+
+        do {
+            const action = this.undoStack.shift();
+            if (!action) { return; }
+
+            if (remaining === null) {
+                remaining = action.groupSize || 1;
+            }
+            remaining--;
+
+            action.isRedo = false;
+            this.distributeUndoRedo(action);
+
+        } while (remaining > 0);
     }
 
     requestRedo() {
-        const redo = this.redoStack.shift();
-        if (redo) {
-            redo.isRedo = true;
-            this.distributeUndoRedo(redo);
+        let remaining = null;
+
+        do {
+            const action = this.redoStack.shift();
+            if (!action) { return; }
+
+            if (remaining === null) {
+                remaining = action.groupSize || 1;
+            }
+            remaining--;
+
+            action.isRedo = true;
+            this.distributeUndoRedo(action);
+
+        } while (remaining > 0);
+    }
+
+    // Calculate stack action count taking groupSize (atomic action
+    // sets) into consideration.
+    stackCount(stack: UndoRedoAction[]): number {
+        let size = 0;
+        let skip = 0;
+
+        stack.forEach(action => {
+            if (action.groupSize > 1) {
+                if (skip) { return; }
+                skip = 1;
+            } else {
+                skip = 0;
+            }
+            size++;
+        });
+
+        return size;
+    }
+
+    undoCount(): number {
+        return this.stackCount(this.undoStack);
+    }
+
+    redoCount(): number {
+        return this.stackCount(this.redoStack);
+    }
+
+    // Stamp the most recent 'size' entries in the undo stack
+    // as being an atomic undo/redo set.
+    setUndoGroupSize(size: number) {
+        for (let idx = 0; idx < size; idx++) {
+            if (this.undoStack[idx]) {
+                this.undoStack[idx].groupSize = size;
+            }
         }
     }
 
@@ -117,6 +193,11 @@ export class MarcEditContext {
         }
     }
 
+    addToUndoStack(action: UndoRedoAction) {
+        this.changesPending = true;
+        this.undoStack.unshift(action);
+    }
+
     handleStructuralUndoRedo(action: StructUndoRedoAction) {
 
         if (action.wasAddition) {
@@ -137,24 +218,24 @@ export class MarcEditContext {
 
         } else {
             // Re-insert the removed field and focus it.
-            
-            if (action.subfield) { 
+
+            if (action.subfield) {
 
                 this.insertSubfield(action.field, action.subfield, true);
                 this.focusSubfield(action.field, action.subfield[2]);
 
             } else {
-                
+
                 const fieldId = action.position.fieldId;
-                const prevField = 
+                const prevField =
                     this.record.getField(action.prevPosition.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);
             }
@@ -208,20 +289,22 @@ export class MarcEditContext {
         // time of action will be different than the added field.
         action.prevFocus = this.lastFocused;
 
-        this.undoStack.unshift(action);
+        this.addToUndoStack(action);
     }
 
-    deleteField(field: MarcField) { 
+    deleteField(field: MarcField) {
         this.trackStructuralUndo(field, false);
 
-        this.focusNextTag(field) || this.focusPreviousTag(field);
+        if (!this.focusNextTag(field)) {
+            this.focusPreviousTag(field);
+        }
 
         this.record.deleteFields(field);
     }
 
     add00X(tag: string) {
 
-        const field: MarcField = 
+        const field: MarcField =
             this.record.newField({tag : tag, data : STUB_DATA_00X});
 
         this.record.insertOrderedFields(field);
@@ -274,7 +357,7 @@ export class MarcEditContext {
 
     // Adds a new empty subfield to the provided field at the
     // requested subfield position
-    insertSubfield(field: MarcField, 
+    insertSubfield(field: MarcField,
         subfield: MarcSubfield, skipTracking?: boolean) {
         const position = subfield[2];
 
@@ -297,18 +380,18 @@ export class MarcEditContext {
         const newSf: MarcSubfield = [' ', '', position];
         this.insertSubfield(field, newSf);
     }
-    
-    // Focus the requested subfield by its position.  If its 
+
+    // Focus the requested subfield by its position.  If its
     // position is less than zero, focus the field's tag instead.
     focusSubfield(field: MarcField, position: number) {
 
         const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
 
-        if (position >= 0) { 
+        if (position >= 0) {
             // Focus the code instead of the value, because attempting to
             // focus an empty (editable) div results in nothing getting focus.
             focus.target = 'sfc';
-            focus.sfOffset = position; 
+            focus.sfOffset = position;
         }
 
         this.requestFieldFocus(focus);
@@ -331,8 +414,8 @@ export class MarcEditContext {
     // Returns true if the field has a next tag to focus
     focusNextTag(field: MarcField) {
         const nextField = this.record.getNextField(field.fieldId);
-        if (nextField) { 
-            this.focusTag(nextField); 
+        if (nextField) {
+            this.focusTag(nextField);
             return true;
         }
         return false;