]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/eg2/src/app/staff/share/marc-edit/editor-context.ts
16a8caeeccbfe05d04d8a70772f94e8ca0fe2e5e
[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     // Grouped actions are tracked as multiple undo / redo actions, but
27     // are done and un-done as a unit.
28     groupSize?: number;
29 }
30
31 export class TextUndoRedoAction extends UndoRedoAction {
32     textContent: string;
33 }
34
35 export class StructUndoRedoAction extends UndoRedoAction {
36     /* Add or remove a part of the record (field, subfield, etc.) */
37
38     // Does this action track an addition or deletion.
39     wasAddition: boolean;
40
41     // Field to add/delete or field to modify for subfield adds/deletes
42     field: MarcField;
43
44     // If this is a subfield modification.
45     subfield: MarcSubfield;
46
47     // Position preceding the modified position to mark the position
48     // of deletion recovery.
49     prevPosition: FieldFocusRequest;
50
51     // Location of the cursor at time of initial action.
52     prevFocus: FieldFocusRequest;
53 }
54
55
56 export class MarcEditContext {
57
58     recordChange: EventEmitter<MarcRecord>;
59     fieldFocusRequest: EventEmitter<FieldFocusRequest>;
60     textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
61     recordType: 'biblio' | 'authority' = 'biblio';
62
63     lastFocused: FieldFocusRequest = null;
64
65     undoStack: UndoRedoAction[] = [];
66     redoStack: UndoRedoAction[] = [];
67
68     // True if any changes have been made.
69     // For the 'rich' editor, this is any un-do-able actions.
70     // For the text edtior it's any text change.
71     changesPending: boolean;
72
73     private _record: MarcRecord;
74     set record(r: MarcRecord) {
75         if (r !== this._record) {
76             this._record = r;
77             this._record.stampFieldIds();
78             this.recordChange.emit(r);
79         }
80     }
81
82     get record(): MarcRecord {
83         return this._record;
84     }
85
86     constructor() {
87         this.recordChange = new EventEmitter<MarcRecord>();
88         this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
89         this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
90     }
91
92     requestFieldFocus(req: FieldFocusRequest) {
93         // timeout allows for new components to be built before the
94         // focus request is emitted.
95         if (req) {
96             setTimeout(() => this.fieldFocusRequest.emit(req));
97         }
98     }
99
100     resetUndos() {
101         this.undoStack = [];
102         this.redoStack = [];
103     }
104
105     requestUndo() {
106         let remaining = null;
107
108         do {
109             const action = this.undoStack.shift();
110             if (!action) { return; }
111
112             if (remaining === null) {
113                 remaining = action.groupSize || 1;
114             }
115             remaining--;
116
117             action.isRedo = false;
118             this.distributeUndoRedo(action);
119
120         } while (remaining > 0);
121     }
122
123     requestRedo() {
124         let remaining = null;
125
126         do {
127             const action = this.redoStack.shift();
128             if (!action) { return; }
129
130             if (remaining === null) {
131                 remaining = action.groupSize || 1;
132             }
133             remaining--;
134
135             action.isRedo = true;
136             this.distributeUndoRedo(action);
137
138         } while (remaining > 0);
139     }
140
141     // Calculate stack action count taking groupSize (atomic action
142     // sets) into consideration.
143     stackCount(stack: UndoRedoAction[]): number {
144         let size = 0;
145         let skip = 0;
146
147         stack.forEach(action => {
148             if (action.groupSize > 1) {
149                 if (skip) { return; }
150                 skip = 1;
151             } else {
152                 skip = 0;
153             }
154             size++;
155         });
156
157         return size;
158     }
159
160     undoCount(): number {
161         return this.stackCount(this.undoStack);
162     }
163
164     redoCount(): number {
165         return this.stackCount(this.redoStack);
166     }
167
168     // Stamp the most recent 'size' entries in the undo stack
169     // as being an atomic undo/redo set.
170     setUndoGroupSize(size: number) {
171         for (let idx = 0; idx < size; idx++) {
172             if (this.undoStack[idx]) {
173                 this.undoStack[idx].groupSize = size;
174             }
175         }
176     }
177
178     distributeUndoRedo(action: UndoRedoAction) {
179         if (action instanceof TextUndoRedoAction) {
180             // Let the editable content component handle it.
181             this.textUndoRedoRequest.emit(action);
182         } else {
183             // Manage structural changes within
184             this.handleStructuralUndoRedo(action as StructUndoRedoAction);
185         }
186     }
187
188     addToUndoStack(action: UndoRedoAction) {
189         this.changesPending = true;
190         this.undoStack.unshift(action);
191     }
192
193     handleStructuralUndoRedo(action: StructUndoRedoAction) {
194
195         if (action.wasAddition) {
196             // Remove the added field
197
198             if (action.subfield) {
199                 const prevPos = action.subfield[2] - 1;
200                 action.field.deleteExactSubfields(action.subfield);
201                 this.focusSubfield(action.field, prevPos);
202
203             } else {
204                 this.record.deleteFields(action.field);
205             }
206
207             // When deleting chunks, always return focus to the
208             // pre-insert position.
209             this.requestFieldFocus(action.prevFocus);
210
211         } else {
212             // Re-insert the removed field and focus it.
213
214             if (action.subfield) {
215
216                 this.insertSubfield(action.field, action.subfield, true);
217                 this.focusSubfield(action.field, action.subfield[2]);
218
219             } else {
220
221                 const fieldId = action.position.fieldId;
222                 const prevField =
223                     this.record.getField(action.prevPosition.fieldId);
224
225                 this.record.insertFieldsAfter(prevField, action.field);
226
227                 // Recover the original fieldId, which gets re-stamped
228                 // in this.record.insertFields* calls.
229                 action.field.fieldId = fieldId;
230
231                 // Focus the newly recovered field.
232                 this.requestFieldFocus(action.position);
233             }
234
235             // When inserting chunks, track the location where the
236             // insert was requested so we can return the cursor so we
237             // can return the cursor to the scene of the crime if the
238             // undo is re-done or vice versa.  This is primarily useful
239             // when performing global inserts like add00X, which can be
240             // done without the 00X field itself having focus.
241             action.prevFocus = this.lastFocused;
242         }
243
244         action.wasAddition = !action.wasAddition;
245
246         const moveTo = action.isRedo ? this.undoStack : this.redoStack;
247
248         moveTo.unshift(action);
249     }
250
251     trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
252
253         // Human-driven changes invalidate the redo stack.
254         this.redoStack = [];
255
256         const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
257
258         let prevPos: FieldFocusRequest = null;
259
260         if (subfield) {
261             position.target = 'sfc';
262             position.sfOffset = subfield[2];
263
264         } else {
265             // No need to track the previous field for subfield mods.
266
267             const prevField = this.record.getPreviousField(field.fieldId);
268             if (prevField) {
269                 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
270             }
271         }
272
273         const action = new StructUndoRedoAction();
274         action.field = field;
275         action.subfield = subfield;
276         action.wasAddition = isAddition;
277         action.position = position;
278         action.prevPosition = prevPos;
279
280         // For bulk adds (e.g. add a whole row) the field focused at
281         // time of action will be different than the added field.
282         action.prevFocus = this.lastFocused;
283
284         this.addToUndoStack(action);
285     }
286
287     deleteField(field: MarcField) {
288         this.trackStructuralUndo(field, false);
289
290         if (!this.focusNextTag(field)) {
291             this.focusPreviousTag(field);
292         }
293
294         this.record.deleteFields(field);
295     }
296
297     add00X(tag: string) {
298
299         const field: MarcField =
300             this.record.newField({tag : tag, data : STUB_DATA_00X});
301
302         this.record.insertOrderedFields(field);
303
304         this.trackStructuralUndo(field, true);
305
306         this.focusTag(field);
307     }
308
309     insertReplace008() {
310
311         // delete all of the 008s
312         [].concat(this.record.field('008', true)).forEach(f => {
313             this.trackStructuralUndo(f, false);
314             this.record.deleteFields(f);
315         });
316
317         const field = this.record.newField({
318             tag : '008', data : this.record.generate008()});
319
320         this.record.insertOrderedFields(field);
321
322         this.trackStructuralUndo(field, true);
323
324         this.focusTag(field);
325     }
326
327     // Add stub field before or after the context field
328     insertStubField(field: MarcField, before?: boolean) {
329
330         const newField = this.record.newField(
331             {tag: '999', subfields: [[' ', '', 0]]});
332
333         this.insertField(field, newField, before);
334     }
335
336     insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
337
338         if (before) {
339             this.record.insertFieldsBefore(contextField, newField);
340             this.focusPreviousTag(contextField);
341
342         } else {
343             this.record.insertFieldsAfter(contextField, newField);
344             this.focusNextTag(contextField);
345         }
346
347         this.trackStructuralUndo(newField, true);
348     }
349
350     // Adds a new empty subfield to the provided field at the
351     // requested subfield position
352     insertSubfield(field: MarcField,
353         subfield: MarcSubfield, skipTracking?: boolean) {
354         const position = subfield[2];
355
356         // array index 3 contains that position of the subfield
357         // in the MARC field.  When splicing a new subfield into
358         // the set, be sure the any that come after the new one
359         // have their positions bumped to reflect the shift.
360         field.subfields.forEach(
361             sf => {if (sf[2] >= position) { sf[2]++; }});
362
363         field.subfields.splice(position, 0, subfield);
364
365         if (!skipTracking) {
366             this.focusSubfield(field, position);
367             this.trackStructuralUndo(field, true, subfield);
368         }
369     }
370
371     insertStubSubfield(field: MarcField, position: number) {
372         const newSf: MarcSubfield = [' ', '', position];
373         this.insertSubfield(field, newSf);
374     }
375
376     // Focus the requested subfield by its position.  If its
377     // position is less than zero, focus the field's tag instead.
378     focusSubfield(field: MarcField, position: number) {
379
380         const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
381
382         if (position >= 0) {
383             // Focus the code instead of the value, because attempting to
384             // focus an empty (editable) div results in nothing getting focus.
385             focus.target = 'sfc';
386             focus.sfOffset = position;
387         }
388
389         this.requestFieldFocus(focus);
390     }
391
392     deleteSubfield(field: MarcField, subfield: MarcSubfield) {
393         const sfpos = subfield[2] - 1; // previous subfield
394
395         this.trackStructuralUndo(field, false, subfield);
396
397         field.deleteExactSubfields(subfield);
398
399         this.focusSubfield(field, sfpos);
400     }
401
402     focusTag(field: MarcField) {
403         this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
404     }
405
406     // Returns true if the field has a next tag to focus
407     focusNextTag(field: MarcField) {
408         const nextField = this.record.getNextField(field.fieldId);
409         if (nextField) {
410             this.focusTag(nextField);
411             return true;
412         }
413         return false;
414     }
415
416     // Returns true if the field has a previous tag to focus
417     focusPreviousTag(field: MarcField): boolean {
418         const prevField = this.record.getPreviousField(field.fieldId);
419         if (prevField) {
420             this.focusTag(prevField);
421             return true;
422         }
423         return false;
424     }
425 }
426