1 import {EventEmitter} from '@angular/core';
2 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
3 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
4 import {TagTable} from './tagtable.service';
6 /* Per-instance MARC editor context. */
8 const STUB_DATA_00X = ' ';
10 export type MARC_EDITABLE_FIELD_TYPE =
11 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
13 export interface FieldFocusRequest {
15 target: MARC_EDITABLE_FIELD_TYPE;
16 sfOffset?: number; // focus a specific subfield by its offset
17 ffCode?: string; // fixed field code
19 // If set, an external source wants to modify the text content
20 // of an editable component (in a way that retains undo/redo
25 export class UndoRedoAction {
26 // Which point in the record was modified.
27 position: FieldFocusRequest;
29 // Which stack do we toss this on once it's been applied?
32 // Grouped actions are tracked as multiple undo / redo actions, but
33 // are done and un-done as a unit.
37 export class TextUndoRedoAction extends UndoRedoAction {
41 export class StructUndoRedoAction extends UndoRedoAction {
42 /* Add or remove a part of the record (field, subfield, etc.) */
44 // Does this action track an addition or deletion.
47 // Field to add/delete or field to modify for subfield adds/deletes
50 // If this is a subfield modification.
51 subfield: MarcSubfield;
53 // Position preceding the modified position to mark the position
54 // of deletion recovery.
55 prevPosition: FieldFocusRequest;
57 // Location of the cursor at time of initial action.
58 prevFocus: FieldFocusRequest;
62 export class MarcEditContext {
64 recordChange: EventEmitter<MarcRecord>;
65 fieldFocusRequest: EventEmitter<FieldFocusRequest>;
66 textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
67 recordType: 'biblio' | 'authority' = 'biblio';
69 lastFocused: FieldFocusRequest = null;
71 undoStack: UndoRedoAction[] = [];
72 redoStack: UndoRedoAction[] = [];
76 // True if any changes have been made.
77 // For the 'rich' editor, this is any un-do-able actions.
78 // For the text edtior it's any text change.
79 changesPending: boolean;
81 private _record: MarcRecord;
82 set record(r: MarcRecord) {
83 if (r !== this._record) {
85 this._record.stampFieldIds();
86 this.recordChange.emit(r);
90 get record(): MarcRecord {
95 this.recordChange = new EventEmitter<MarcRecord>();
96 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
97 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
100 requestFieldFocus(req: FieldFocusRequest) {
101 // timeout allows for new components to be built before the
102 // focus request is emitted.
104 setTimeout(() => this.fieldFocusRequest.emit(req));
114 let remaining = null;
117 const action = this.undoStack.shift();
118 if (!action) { return; }
120 if (remaining === null) {
121 remaining = action.groupSize || 1;
125 action.isRedo = false;
126 this.distributeUndoRedo(action);
128 } while (remaining > 0);
132 let remaining = null;
135 const action = this.redoStack.shift();
136 if (!action) { return; }
138 if (remaining === null) {
139 remaining = action.groupSize || 1;
143 action.isRedo = true;
144 this.distributeUndoRedo(action);
146 } while (remaining > 0);
149 // Calculate stack action count taking groupSize (atomic action
150 // sets) into consideration.
151 stackCount(stack: UndoRedoAction[]): number {
155 stack.forEach(action => {
156 if (action.groupSize > 1) {
157 if (skip) { return; }
168 undoCount(): number {
169 return this.stackCount(this.undoStack);
172 redoCount(): number {
173 return this.stackCount(this.redoStack);
176 // Stamp the most recent 'size' entries in the undo stack
177 // as being an atomic undo/redo set.
178 setUndoGroupSize(size: number) {
179 for (let idx = 0; idx < size; idx++) {
180 if (this.undoStack[idx]) {
181 this.undoStack[idx].groupSize = size;
186 distributeUndoRedo(action: UndoRedoAction) {
187 if (action instanceof TextUndoRedoAction) {
188 // Let the editable content component handle it.
189 this.textUndoRedoRequest.emit(action);
191 // Manage structural changes within
192 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
196 addToUndoStack(action: UndoRedoAction) {
197 this.changesPending = true;
198 this.undoStack.unshift(action);
201 handleStructuralUndoRedo(action: StructUndoRedoAction) {
203 if (action.wasAddition) {
204 // Remove the added field
206 if (action.subfield) {
207 const prevPos = action.subfield[2] - 1;
208 action.field.deleteExactSubfields(action.subfield);
209 this.focusSubfield(action.field, prevPos);
212 this.record.deleteFields(action.field);
215 // When deleting chunks, always return focus to the
216 // pre-insert position.
217 this.requestFieldFocus(action.prevFocus);
220 // Re-insert the removed field and focus it.
222 if (action.subfield) {
224 this.insertSubfield(action.field, action.subfield, true);
225 this.focusSubfield(action.field, action.subfield[2]);
229 const fieldId = action.position.fieldId;
231 this.record.getField(action.prevPosition.fieldId);
233 this.record.insertFieldsAfter(prevField, action.field);
235 // Recover the original fieldId, which gets re-stamped
236 // in this.record.insertFields* calls.
237 action.field.fieldId = fieldId;
239 // Focus the newly recovered field.
240 this.requestFieldFocus(action.position);
243 // When inserting chunks, track the location where the
244 // insert was requested so we can return the cursor so we
245 // can return the cursor to the scene of the crime if the
246 // undo is re-done or vice versa. This is primarily useful
247 // when performing global inserts like add00X, which can be
248 // done without the 00X field itself having focus.
249 action.prevFocus = this.lastFocused;
252 action.wasAddition = !action.wasAddition;
254 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
256 moveTo.unshift(action);
259 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
261 // Human-driven changes invalidate the redo stack.
264 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
266 let prevPos: FieldFocusRequest = null;
269 position.target = 'sfc';
270 position.sfOffset = subfield[2];
273 // No need to track the previous field for subfield mods.
275 const prevField = this.record.getPreviousField(field.fieldId);
277 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
281 const action = new StructUndoRedoAction();
282 action.field = field;
283 action.subfield = subfield;
284 action.wasAddition = isAddition;
285 action.position = position;
286 action.prevPosition = prevPos;
288 // For bulk adds (e.g. add a whole row) the field focused at
289 // time of action will be different than the added field.
290 action.prevFocus = this.lastFocused;
292 this.addToUndoStack(action);
295 deleteField(field: MarcField) {
296 this.trackStructuralUndo(field, false);
298 if (!this.focusNextTag(field)) {
299 this.focusPreviousTag(field);
302 this.record.deleteFields(field);
305 add00X(tag: string) {
307 const field: MarcField =
308 this.record.newField({tag : tag, data : STUB_DATA_00X});
310 this.record.insertOrderedFields(field);
312 this.trackStructuralUndo(field, true);
314 this.focusTag(field);
319 // delete all of the 008s
320 [].concat(this.record.field('008', true)).forEach(f => {
321 this.trackStructuralUndo(f, false);
322 this.record.deleteFields(f);
325 const field = this.record.newField({
326 tag : '008', data : this.record.generate008()});
328 this.record.insertOrderedFields(field);
330 this.trackStructuralUndo(field, true);
332 this.focusTag(field);
335 // Add stub field before or after the context field
336 insertStubField(field: MarcField, before?: boolean) {
338 const newField = this.record.newField(
339 {tag: '999', subfields: [[' ', '', 0]]});
341 this.insertField(field, newField, before);
344 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
347 this.record.insertFieldsBefore(contextField, newField);
348 this.focusPreviousTag(contextField);
351 this.record.insertFieldsAfter(contextField, newField);
352 this.focusNextTag(contextField);
355 this.trackStructuralUndo(newField, true);
358 // Adds a new empty subfield to the provided field at the
359 // requested subfield position
360 insertSubfield(field: MarcField,
361 subfield: MarcSubfield, skipTracking?: boolean) {
362 const position = subfield[2];
364 // array index 3 contains that position of the subfield
365 // in the MARC field. When splicing a new subfield into
366 // the set, be sure the any that come after the new one
367 // have their positions bumped to reflect the shift.
368 field.subfields.forEach(
369 sf => {if (sf[2] >= position) { sf[2]++; }});
371 field.subfields.splice(position, 0, subfield);
374 this.focusSubfield(field, position);
375 this.trackStructuralUndo(field, true, subfield);
379 insertStubSubfield(field: MarcField, position: number) {
380 const newSf: MarcSubfield = [' ', '', position];
381 this.insertSubfield(field, newSf);
384 // Focus the requested subfield by its position. If its
385 // position is less than zero, focus the field's tag instead.
386 focusSubfield(field: MarcField, position: number) {
388 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
391 // Focus the code instead of the value, because attempting to
392 // focus an empty (editable) div results in nothing getting focus.
393 focus.target = 'sfc';
394 focus.sfOffset = position;
397 this.requestFieldFocus(focus);
400 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
401 const sfpos = subfield[2] - 1; // previous subfield
403 this.trackStructuralUndo(field, false, subfield);
405 field.deleteExactSubfields(subfield);
407 this.focusSubfield(field, sfpos);
410 focusTag(field: MarcField) {
411 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
414 // Returns true if the field has a next tag to focus
415 focusNextTag(field: MarcField) {
416 const nextField = this.record.getNextField(field.fieldId);
418 this.focusTag(nextField);
424 // Returns true if the field has a previous tag to focus
425 focusPreviousTag(field: MarcField): boolean {
426 const prevField = this.record.getPreviousField(field.fieldId);
428 this.focusTag(prevField);