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