From: Bill Erickson Date: Tue, 3 Dec 2019 23:14:34 +0000 (-0500) Subject: LPXXX undo/redo X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=89f3d4a5c3b8feb79e7e60ff4b75e505ff124deb;p=working%2FEvergreen.git LPXXX undo/redo Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.html b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.html index 25289e102b..359128cee8 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.html @@ -9,6 +9,7 @@ (menuItemSelected)="contextMenuChange($event.value)" (keydown)="inputKeyDown($event)" (focus)="selectText()" + (blur)="inputBlurred()" (input)="bigTextValueChange()"> @@ -26,6 +27,7 @@ (menuItemSelected)="contextMenuChange($event.value)" (keydown)="inputKeyDown($event)" (focus)="selectText()" + (blur)="inputBlurred()" [ngModel]="getContent()" (ngModelChange)="setContent($event)" /> diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts index 9b2c98bb3f..0005a78b94 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editable-content.component.ts @@ -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(); } diff --git a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts index f4f6c1281a..90448cbb63 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts @@ -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; fieldFocusRequest: EventEmitter; - undoRequest: EventEmitter; - redoRequest: EventEmitter; + undoRedoRequest: EventEmitter; recordType: 'biblio' | 'authority' = 'biblio'; lastFocused: FieldFocusRequest = null; @@ -54,8 +63,7 @@ export class MarcEditContext { constructor() { this.recordChange = new EventEmitter(); this.fieldFocusRequest = new EventEmitter(); - this.undoRequest = new EventEmitter(); - this.redoRequest = new EventEmitter(); + this.undoRedoRequest = new EventEmitter(); } 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;