-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';
styleUrls: ['./editable-content.component.css']
})
-export class EditableContentComponent implements OnInit, AfterViewInit {
+export class EditableContentComponent
+ implements OnInit, AfterViewInit, OnDestroy {
@Input() context: MarcEditContext;
@Input() field: MarcField;
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,
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) {
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,
setupFieldType() {
const content = this.getContent();
+ this.initialContent = content;
switch (this.fieldType) {
case 'ldr':
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;
}
}
}
}
- setContent(value: string) {
+ setContent(value: string, propagatBigText?: boolean, skipUndoTrack?: boolean) {
if (this.fieldText) { return; } // read-only text
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
}
break;
+ case 'z':
+ if (evt.ctrlKey) { // undo
+ this.context.requestUndo();
+ evt.preventDefault();
+ }
+ break;
+
}
}
}
contextMenuChange(value: string) {
- this.setContent(value);
- if (this.bigText) {
- // propagate to div
- this.editInput.innerText = value;
- }
+ this.setContent(value, true);
}
}
}
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;
constructor() {
this.recordChange = new EventEmitter<MarcRecord>();
this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
+ this.undoRequest = new EventEmitter<UndoRedoAction>();
+ this.redoRequest = new EventEmitter<UndoRedoAction>();
}
requestFieldFocus(req: FieldFocusRequest) {
});
}
+ // 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) {
this.getFieldOffset(this.lastFocused.fieldId);
this.undoStack.unshift({
- breakerText: this.record.toBreaker(),
position: this.lastFocused
});
}