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 const MARC_RECORD_TYPES: 'biblio' | 'authority' | 'serial' | 'lineitem' = null;
8 export type MARC_RECORD_TYPE = typeof MARC_RECORD_TYPES;
10 /* Per-instance MARC editor context. */
12 const STUB_DATA_00X = ' ';
14 export type MARC_EDITABLE_FIELD_TYPE =
15 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
17 export interface FieldFocusRequest {
19 target: MARC_EDITABLE_FIELD_TYPE;
20 sfOffset?: number; // focus a specific subfield by its offset
21 ffCode?: string; // fixed field code
23 // If set, an external source wants to modify the text content
24 // of an editable component (in a way that retains undo/redo
29 export class UndoRedoAction {
30 // Which point in the record was modified.
31 position: FieldFocusRequest;
33 // Which stack do we toss this on once it's been applied?
36 // Grouped actions are tracked as multiple undo / redo actions, but
37 // are done and un-done as a unit.
41 export class TextUndoRedoAction extends UndoRedoAction {
45 export class StructUndoRedoAction extends UndoRedoAction {
46 /* Add or remove a part of the record (field, subfield, etc.) */
48 // Does this action track an addition or deletion.
51 // Field to add/delete or field to modify for subfield adds/deletes
54 // If this is a subfield modification.
55 subfield: MarcSubfield;
57 // Position preceding the modified position to mark the position
58 // of deletion recovery.
59 prevPosition: FieldFocusRequest;
61 // Location of the cursor at time of initial action.
62 prevFocus: FieldFocusRequest;
66 export class MarcEditContext {
68 recordChange: EventEmitter<MarcRecord>;
69 fieldFocusRequest: EventEmitter<FieldFocusRequest>;
70 textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
71 recordType: MARC_RECORD_TYPE;
73 lastFocused: FieldFocusRequest = null;
75 undoStack: UndoRedoAction[] = [];
76 redoStack: UndoRedoAction[] = [];
80 // True if any changes have been made.
81 // For the 'rich' editor, this is any un-do-able actions.
82 // For the text edtior it's any text change.
83 changesPending: boolean;
85 private _record: MarcRecord;
86 set record(r: MarcRecord) {
87 if (r !== this._record) {
89 this._record.stampFieldIds();
90 this.recordChange.emit(r);
94 get record(): MarcRecord {
99 this.recordChange = new EventEmitter<MarcRecord>();
100 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
101 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
104 requestFieldFocus(req: FieldFocusRequest) {
105 // timeout allows for new components to be built before the
106 // focus request is emitted.
108 setTimeout(() => this.fieldFocusRequest.emit(req));
118 let remaining = null;
121 const action = this.undoStack.shift();
122 if (!action) { return; }
124 if (remaining === null) {
125 remaining = action.groupSize || 1;
129 action.isRedo = false;
130 this.distributeUndoRedo(action);
132 } while (remaining > 0);
136 let remaining = null;
139 const action = this.redoStack.shift();
140 if (!action) { return; }
142 if (remaining === null) {
143 remaining = action.groupSize || 1;
147 action.isRedo = true;
148 this.distributeUndoRedo(action);
150 } while (remaining > 0);
153 // Calculate stack action count taking groupSize (atomic action
154 // sets) into consideration.
155 stackCount(stack: UndoRedoAction[]): number {
159 stack.forEach(action => {
160 if (action.groupSize > 1) {
161 if (skip) { return; }
172 undoCount(): number {
173 return this.stackCount(this.undoStack);
176 redoCount(): number {
177 return this.stackCount(this.redoStack);
180 // Stamp the most recent 'size' entries in the undo stack
181 // as being an atomic undo/redo set.
182 setUndoGroupSize(size: number) {
183 for (let idx = 0; idx < size; idx++) {
184 if (this.undoStack[idx]) {
185 this.undoStack[idx].groupSize = size;
190 distributeUndoRedo(action: UndoRedoAction) {
191 if (action instanceof TextUndoRedoAction) {
192 // Let the editable content component handle it.
193 this.textUndoRedoRequest.emit(action);
195 // Manage structural changes within
196 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
200 addToUndoStack(action: UndoRedoAction) {
201 this.changesPending = true;
202 this.undoStack.unshift(action);
205 handleStructuralUndoRedo(action: StructUndoRedoAction) {
207 if (action.wasAddition) {
208 // Remove the added field
210 if (action.subfield) {
211 const prevPos = action.subfield[2] - 1;
212 action.field.deleteExactSubfields(action.subfield);
213 this.focusSubfield(action.field, prevPos);
216 this.record.deleteFields(action.field);
219 // When deleting chunks, always return focus to the
220 // pre-insert position.
221 this.requestFieldFocus(action.prevFocus);
224 // Re-insert the removed field and focus it.
226 if (action.subfield) {
228 this.insertSubfield(action.field, action.subfield, true);
229 this.focusSubfield(action.field, action.subfield[2]);
233 const fieldId = action.position.fieldId;
235 this.record.getField(action.prevPosition.fieldId);
237 this.record.insertFieldsAfter(prevField, action.field);
239 // Recover the original fieldId, which gets re-stamped
240 // in this.record.insertFields* calls.
241 action.field.fieldId = fieldId;
243 // Focus the newly recovered field.
244 this.requestFieldFocus(action.position);
247 // When inserting chunks, track the location where the
248 // insert was requested so we can return the cursor so we
249 // can return the cursor to the scene of the crime if the
250 // undo is re-done or vice versa. This is primarily useful
251 // when performing global inserts like add00X, which can be
252 // done without the 00X field itself having focus.
253 action.prevFocus = this.lastFocused;
256 action.wasAddition = !action.wasAddition;
258 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
260 moveTo.unshift(action);
263 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
265 // Human-driven changes invalidate the redo stack.
268 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
270 let prevPos: FieldFocusRequest = null;
273 position.target = 'sfc';
274 position.sfOffset = subfield[2];
277 // No need to track the previous field for subfield mods.
279 const prevField = this.record.getPreviousField(field.fieldId);
281 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
285 const action = new StructUndoRedoAction();
286 action.field = field;
287 action.subfield = subfield;
288 action.wasAddition = isAddition;
289 action.position = position;
290 action.prevPosition = prevPos;
292 // For bulk adds (e.g. add a whole row) the field focused at
293 // time of action will be different than the added field.
294 action.prevFocus = this.lastFocused;
296 this.addToUndoStack(action);
299 deleteField(field: MarcField) {
300 this.trackStructuralUndo(field, false);
302 if (!this.focusNextTag(field)) {
303 this.focusPreviousTag(field);
306 this.record.deleteFields(field);
309 add00X(tag: string) {
311 const field: MarcField =
312 this.record.newField({tag : tag, data : STUB_DATA_00X});
314 this.record.insertOrderedFields(field);
316 this.trackStructuralUndo(field, true);
318 this.focusTag(field);
323 // delete all of the 008s
324 [].concat(this.record.field('008', true)).forEach(f => {
325 this.trackStructuralUndo(f, false);
326 this.record.deleteFields(f);
329 const field = this.record.newField({
330 tag : '008', data : this.record.generate008()});
332 this.record.insertOrderedFields(field);
334 this.trackStructuralUndo(field, true);
336 this.focusTag(field);
339 // Add stub field before or after the context field
340 insertStubField(field: MarcField, before?: boolean) {
342 const newField = this.record.newField(
343 {tag: '999', subfields: [[' ', '', 0]]});
345 this.insertField(field, newField, before);
348 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
351 this.record.insertFieldsBefore(contextField, newField);
352 this.focusPreviousTag(contextField);
355 this.record.insertFieldsAfter(contextField, newField);
356 this.focusNextTag(contextField);
359 this.trackStructuralUndo(newField, true);
362 // Adds a new empty subfield to the provided field at the
363 // requested subfield position
364 insertSubfield(field: MarcField,
365 subfield: MarcSubfield, skipTracking?: boolean) {
366 const position = subfield[2];
368 // array index 3 contains that position of the subfield
369 // in the MARC field. When splicing a new subfield into
370 // the set, be sure the any that come after the new one
371 // have their positions bumped to reflect the shift.
372 field.subfields.forEach(
373 sf => {if (sf[2] >= position) { sf[2]++; }});
375 field.subfields.splice(position, 0, subfield);
378 this.focusSubfield(field, position);
379 this.trackStructuralUndo(field, true, subfield);
383 insertStubSubfield(field: MarcField, position: number) {
384 const newSf: MarcSubfield = [' ', '', position];
385 this.insertSubfield(field, newSf);
388 // Focus the requested subfield by its position. If its
389 // position is less than zero, focus the field's tag instead.
390 focusSubfield(field: MarcField, position: number) {
392 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
395 // Focus the code instead of the value, because attempting to
396 // focus an empty (editable) div results in nothing getting focus.
397 focus.target = 'sfc';
398 focus.sfOffset = position;
401 this.requestFieldFocus(focus);
404 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
405 const sfpos = subfield[2] - 1; // previous subfield
407 this.trackStructuralUndo(field, false, subfield);
409 field.deleteExactSubfields(subfield);
411 this.focusSubfield(field, sfpos);
414 focusTag(field: MarcField) {
415 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
418 // Returns true if the field has a next tag to focus
419 focusNextTag(field: MarcField) {
420 const nextField = this.record.getNextField(field.fieldId);
422 this.focusTag(nextField);
428 // Returns true if the field has a previous tag to focus
429 focusPreviousTag(field: MarcField): boolean {
430 const prevField = this.record.getPreviousField(field.fieldId);
432 this.focusTag(prevField);