]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts
LP1852782 MARC editor and related lint repairs
[working/Evergreen.git] / Open-ILS / src / eg2 / src / app / staff / share / marc-edit / editor-context.ts
1 import {EventEmitter} from '@angular/core';
2 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
3 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
4
5 /* Per-instance MARC editor context. */
6
7 const STUB_DATA_00X = '                                        ';
8
9 export type MARC_EDITABLE_FIELD_TYPE =
10     'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
11
12 export interface FieldFocusRequest {
13     fieldId: number;
14     target: MARC_EDITABLE_FIELD_TYPE;
15     sfOffset?: number; // focus a specific subfield by its offset
16     ffCode?: string; // fixed field code
17 }
18
19 export class UndoRedoAction {
20     // Which point in the record was modified.
21     position: FieldFocusRequest;
22
23     // Which stack do we toss this on once it's been applied?
24     isRedo: boolean;
25 }
26
27 export class TextUndoRedoAction extends UndoRedoAction {
28     textContent: string;
29 }
30
31 export class StructUndoRedoAction extends UndoRedoAction {
32     /* Add or remove a part of the record (field, subfield, etc.) */
33
34     // Does this action track an addition or deletion.
35     wasAddition: boolean;
36
37     // Field to add/delete or field to modify for subfield adds/deletes
38     field: MarcField;
39
40     // If this is a subfield modification.
41     subfield: MarcSubfield;
42
43     // Position preceding the modified position to mark the position
44     // of deletion recovery.
45     prevPosition: FieldFocusRequest;
46
47     // Location of the cursor at time of initial action.
48     prevFocus: FieldFocusRequest;
49 }
50
51
52 export class MarcEditContext {
53
54     recordChange: EventEmitter<MarcRecord>;
55     fieldFocusRequest: EventEmitter<FieldFocusRequest>;
56     textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
57     recordType: 'biblio' | 'authority' = 'biblio';
58
59     lastFocused: FieldFocusRequest = null;
60
61     undoStack: UndoRedoAction[] = [];
62     redoStack: UndoRedoAction[] = [];
63
64     private _record: MarcRecord;
65     set record(r: MarcRecord) {
66         if (r !== this._record) {
67             this._record = r;
68             this._record.stampFieldIds();
69             this.recordChange.emit(r);
70         }
71     }
72
73     get record(): MarcRecord {
74         return this._record;
75     }
76
77     constructor() {
78         this.recordChange = new EventEmitter<MarcRecord>();
79         this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
80         this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
81     }
82
83     requestFieldFocus(req: FieldFocusRequest) {
84         // timeout allows for new components to be built before the
85         // focus request is emitted.
86         setTimeout(() => this.fieldFocusRequest.emit(req));
87     }
88
89     resetUndos() {
90         this.undoStack = [];
91         this.redoStack = [];
92     }
93
94     requestUndo() {
95         const undo = this.undoStack.shift();
96         if (undo) {
97             undo.isRedo = false;
98             this.distributeUndoRedo(undo);
99         }
100     }
101
102     requestRedo() {
103         const redo = this.redoStack.shift();
104         if (redo) {
105             redo.isRedo = true;
106             this.distributeUndoRedo(redo);
107         }
108     }
109
110     distributeUndoRedo(action: UndoRedoAction) {
111         if (action instanceof TextUndoRedoAction) {
112             // Let the editable content component handle it.
113             this.textUndoRedoRequest.emit(action);
114         } else {
115             // Manage structural changes within
116             this.handleStructuralUndoRedo(action as StructUndoRedoAction);
117         }
118     }
119
120     handleStructuralUndoRedo(action: StructUndoRedoAction) {
121
122         if (action.wasAddition) {
123             // Remove the added field
124
125             if (action.subfield) {
126                 const prevPos = action.subfield[2] - 1;
127                 action.field.deleteExactSubfields(action.subfield);
128                 this.focusSubfield(action.field, prevPos);
129
130             } else {
131                 this.record.deleteFields(action.field);
132             }
133
134             // When deleting chunks, always return focus to the
135             // pre-insert position.
136             this.requestFieldFocus(action.prevFocus);
137
138         } else {
139             // Re-insert the removed field and focus it.
140
141             if (action.subfield) {
142
143                 this.insertSubfield(action.field, action.subfield, true);
144                 this.focusSubfield(action.field, action.subfield[2]);
145
146             } else {
147
148                 const fieldId = action.position.fieldId;
149                 const prevField =
150                     this.record.getField(action.prevPosition.fieldId);
151
152                 this.record.insertFieldsAfter(prevField, action.field);
153
154                 // Recover the original fieldId, which gets re-stamped
155                 // in this.record.insertFields* calls.
156                 action.field.fieldId = fieldId;
157
158                 // Focus the newly recovered field.
159                 this.requestFieldFocus(action.position);
160             }
161
162             // When inserting chunks, track the location where the
163             // insert was requested so we can return the cursor so we
164             // can return the cursor to the scene of the crime if the
165             // undo is re-done or vice versa.  This is primarily useful
166             // when performing global inserts like add00X, which can be
167             // done without the 00X field itself having focus.
168             action.prevFocus = this.lastFocused;
169         }
170
171         action.wasAddition = !action.wasAddition;
172
173         const moveTo = action.isRedo ? this.undoStack : this.redoStack;
174
175         moveTo.unshift(action);
176     }
177
178     trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
179
180         // Human-driven changes invalidate the redo stack.
181         this.redoStack = [];
182
183         const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
184
185         let prevPos: FieldFocusRequest = null;
186
187         if (subfield) {
188             position.target = 'sfc';
189             position.sfOffset = subfield[2];
190
191         } else {
192             // No need to track the previous field for subfield mods.
193
194             const prevField = this.record.getPreviousField(field.fieldId);
195             if (prevField) {
196                 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
197             }
198         }
199
200         const action = new StructUndoRedoAction();
201         action.field = field;
202         action.subfield = subfield;
203         action.wasAddition = isAddition;
204         action.position = position;
205         action.prevPosition = prevPos;
206
207         // For bulk adds (e.g. add a whole row) the field focused at
208         // time of action will be different than the added field.
209         action.prevFocus = this.lastFocused;
210
211         this.undoStack.unshift(action);
212     }
213
214     deleteField(field: MarcField) {
215         this.trackStructuralUndo(field, false);
216
217         if (!this.focusNextTag(field)) {
218             this.focusPreviousTag(field);
219         }
220
221         this.record.deleteFields(field);
222     }
223
224     add00X(tag: string) {
225
226         const field: MarcField =
227             this.record.newField({tag : tag, data : STUB_DATA_00X});
228
229         this.record.insertOrderedFields(field);
230
231         this.trackStructuralUndo(field, true);
232
233         this.focusTag(field);
234     }
235
236     insertReplace008() {
237
238         // delete all of the 008s
239         [].concat(this.record.field('008', true)).forEach(f => {
240             this.trackStructuralUndo(f, false);
241             this.record.deleteFields(f);
242         });
243
244         const field = this.record.newField({
245             tag : '008', data : this.record.generate008()});
246
247         this.record.insertOrderedFields(field);
248
249         this.trackStructuralUndo(field, true);
250
251         this.focusTag(field);
252     }
253
254     // Add stub field before or after the context field
255     insertStubField(field: MarcField, before?: boolean) {
256
257         const newField = this.record.newField(
258             {tag: '999', subfields: [[' ', '', 0]]});
259
260         this.insertField(field, newField, before);
261     }
262
263     insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
264
265         if (before) {
266             this.record.insertFieldsBefore(contextField, newField);
267             this.focusPreviousTag(contextField);
268
269         } else {
270             this.record.insertFieldsAfter(contextField, newField);
271             this.focusNextTag(contextField);
272         }
273
274         this.trackStructuralUndo(newField, true);
275     }
276
277     // Adds a new empty subfield to the provided field at the
278     // requested subfield position
279     insertSubfield(field: MarcField,
280         subfield: MarcSubfield, skipTracking?: boolean) {
281         const position = subfield[2];
282
283         // array index 3 contains that position of the subfield
284         // in the MARC field.  When splicing a new subfield into
285         // the set, be sure the any that come after the new one
286         // have their positions bumped to reflect the shift.
287         field.subfields.forEach(
288             sf => {if (sf[2] >= position) { sf[2]++; }});
289
290         field.subfields.splice(position, 0, subfield);
291
292         if (!skipTracking) {
293             this.focusSubfield(field, position);
294             this.trackStructuralUndo(field, true, subfield);
295         }
296     }
297
298     insertStubSubfield(field: MarcField, position: number) {
299         const newSf: MarcSubfield = [' ', '', position];
300         this.insertSubfield(field, newSf);
301     }
302
303     // Focus the requested subfield by its position.  If its
304     // position is less than zero, focus the field's tag instead.
305     focusSubfield(field: MarcField, position: number) {
306
307         const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
308
309         if (position >= 0) {
310             // Focus the code instead of the value, because attempting to
311             // focus an empty (editable) div results in nothing getting focus.
312             focus.target = 'sfc';
313             focus.sfOffset = position;
314         }
315
316         this.requestFieldFocus(focus);
317     }
318
319     deleteSubfield(field: MarcField, subfield: MarcSubfield) {
320         const sfpos = subfield[2] - 1; // previous subfield
321
322         this.trackStructuralUndo(field, false, subfield);
323
324         field.deleteExactSubfields(subfield);
325
326         this.focusSubfield(field, sfpos);
327     }
328
329     focusTag(field: MarcField) {
330         this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
331     }
332
333     // Returns true if the field has a next tag to focus
334     focusNextTag(field: MarcField) {
335         const nextField = this.record.getNextField(field.fieldId);
336         if (nextField) {
337             this.focusTag(nextField);
338             return true;
339         }
340         return false;
341     }
342
343     // Returns true if the field has a previous tag to focus
344     focusPreviousTag(field: MarcField): boolean {
345         const prevField = this.record.getPreviousField(field.fieldId);
346         if (prevField) {
347             this.focusTag(prevField);
348             return true;
349         }
350         return false;
351     }
352 }
353