// 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,
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() {
.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; }
req = {
fieldId: this.field.fieldId,
target: this.fieldType,
- sfOffset: this.subfield ? this.subfield[2] : null
+ sfOffset: this.subfield ? this.subfield[2] : undefined
};
}
setupFieldType() {
const content = this.getContent();
- this.undoBackToContent = content;
+ this.undoBackToText = content;
switch (this.fieldType) {
case 'ldr':
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;
}
}
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
case 'F6':
if (evt.shiftKey) {
// shift+F6 => add 006
- this.context.add006();
+ this.context.add00X('006');
evt.preventDefault();
evt.stopPropagation();
}
case 'F7':
if (evt.shiftKey) {
// shift+F7 => add 007
- this.context.add007();
+ this.context.add00X('007');
evt.preventDefault();
evt.stopPropagation();
}
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<MarcRecord>;
fieldFocusRequest: EventEmitter<FieldFocusRequest>;
- undoRequest: EventEmitter<UndoRedoAction>;
- redoRequest: EventEmitter<UndoRedoAction>;
+ undoRedoRequest: 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>();
+ this.undoRedoRequest = 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);
+ 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(
// 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
}
insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
- this.recordChanging();
+ //this.recordChanging();
if (before) {
this.record.insertFieldsBefore(contextField, newField);
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;