]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts
168fbda3760059098162497dc88498c6185343c6
[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         this.focusNextTag(field) || this.focusPreviousTag(field);
218
219         this.record.deleteFields(field);
220     }
221
222     add00X(tag: string) {
223
224         const field: MarcField = 
225             this.record.newField({tag : tag, data : STUB_DATA_00X});
226
227         this.record.insertOrderedFields(field);
228
229         this.trackStructuralUndo(field, true);
230
231         this.focusTag(field);
232     }
233
234     insertReplace008() {
235
236         // delete all of the 008s
237         [].concat(this.record.field('008', true)).forEach(f => {
238             this.trackStructuralUndo(f, false);
239             this.record.deleteFields(f);
240         });
241
242         const field = this.record.newField({
243             tag : '008', data : this.record.generate008()});
244
245         this.record.insertOrderedFields(field);
246
247         this.trackStructuralUndo(field, true);
248
249         this.focusTag(field);
250     }
251
252     // Add stub field before or after the context field
253     insertStubField(field: MarcField, before?: boolean) {
254
255         const newField = this.record.newField(
256             {tag: '999', subfields: [[' ', '', 0]]});
257
258         this.insertField(field, newField, before);
259     }
260
261     insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
262
263         if (before) {
264             this.record.insertFieldsBefore(contextField, newField);
265             this.focusPreviousTag(contextField);
266
267         } else {
268             this.record.insertFieldsAfter(contextField, newField);
269             this.focusNextTag(contextField);
270         }
271
272         this.trackStructuralUndo(newField, true);
273     }
274
275     // Adds a new empty subfield to the provided field at the
276     // requested subfield position
277     insertSubfield(field: MarcField, 
278         subfield: MarcSubfield, skipTracking?: boolean) {
279         const position = subfield[2];
280
281         // array index 3 contains that position of the subfield
282         // in the MARC field.  When splicing a new subfield into
283         // the set, be sure the any that come after the new one
284         // have their positions bumped to reflect the shift.
285         field.subfields.forEach(
286             sf => {if (sf[2] >= position) { sf[2]++; }});
287
288         field.subfields.splice(position, 0, subfield);
289
290         if (!skipTracking) {
291             this.focusSubfield(field, position);
292             this.trackStructuralUndo(field, true, subfield);
293         }
294     }
295
296     insertStubSubfield(field: MarcField, position: number) {
297         const newSf: MarcSubfield = [' ', '', position];
298         this.insertSubfield(field, newSf);
299     }
300     
301     // Focus the requested subfield by its position.  If its 
302     // position is less than zero, focus the field's tag instead.
303     focusSubfield(field: MarcField, position: number) {
304
305         const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
306
307         if (position >= 0) { 
308             // Focus the code instead of the value, because attempting to
309             // focus an empty (editable) div results in nothing getting focus.
310             focus.target = 'sfc';
311             focus.sfOffset = position; 
312         }
313
314         this.requestFieldFocus(focus);
315     }
316
317     deleteSubfield(field: MarcField, subfield: MarcSubfield) {
318         const sfpos = subfield[2] - 1; // previous subfield
319
320         this.trackStructuralUndo(field, false, subfield);
321
322         field.deleteExactSubfields(subfield);
323
324         this.focusSubfield(field, sfpos);
325     }
326
327     focusTag(field: MarcField) {
328         this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
329     }
330
331     // Returns true if the field has a next tag to focus
332     focusNextTag(field: MarcField) {
333         const nextField = this.record.getNextField(field.fieldId);
334         if (nextField) { 
335             this.focusTag(nextField); 
336             return true;
337         }
338         return false;
339     }
340
341     // Returns true if the field has a previous tag to focus
342     focusPreviousTag(field: MarcField): boolean {
343         const prevField = this.record.getPreviousField(field.fieldId);
344         if (prevField) {
345             this.focusTag(prevField);
346             return true;
347         }
348         return false;
349     }
350 }
351