From 97e1c920c92256b16625be0f9aa6cc4b0db221a8 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 2 Dec 2019 11:01:39 -0500 Subject: [PATCH] LPXXX text undos Signed-off-by: Bill Erickson --- .../share/marc-edit/editable-content.component.ts | 137 +++++++++++++++++---- .../app/staff/share/marc-edit/editor-context.ts | 18 ++- 2 files changed, 127 insertions(+), 28 deletions(-) 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 ac7f5ebd7b..33209e69cc 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 @@ -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; // or
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); } } 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 11ce5fb2ec..c99f934f36 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 @@ -21,14 +21,16 @@ export interface FieldFocusRequest { } export interface UndoRedoAction { - breakerText: string; position: FieldFocusRequest; + textContent?: string; } export class MarcEditContext { recordChange: EventEmitter; fieldFocusRequest: EventEmitter; + undoRequest: EventEmitter; + redoRequest: EventEmitter; recordType: 'biblio' | 'authority' = 'biblio'; lastFocused: FieldFocusRequest = null; @@ -52,6 +54,8 @@ export class MarcEditContext { constructor() { this.recordChange = new EventEmitter(); this.fieldFocusRequest = new EventEmitter(); + this.undoRequest = new EventEmitter(); + this.redoRequest = new EventEmitter(); } 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 }); } -- 2.11.0