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 /* Per-instance MARC editor context. */
8 const STUB_DATA_00X = ' ';
10 export type MARC_EDITABLE_FIELD_TYPE =
11 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
13 export interface FieldFocusRequest {
15 target: MARC_EDITABLE_FIELD_TYPE;
16 sfOffset?: number; // focus a specific subfield by its offset
17 ffCode?: string; // fixed field code
20 export class UndoRedoAction {
21 // Which point in the record was modified.
22 position: FieldFocusRequest;
24 // Which stack do we toss this on once it's been applied?
27 // Grouped actions are tracked as multiple undo / redo actions, but
28 // are done and un-done as a unit.
32 export class TextUndoRedoAction extends UndoRedoAction {
36 export class StructUndoRedoAction extends UndoRedoAction {
37 /* Add or remove a part of the record (field, subfield, etc.) */
39 // Does this action track an addition or deletion.
42 // Field to add/delete or field to modify for subfield adds/deletes
45 // If this is a subfield modification.
46 subfield: MarcSubfield;
48 // Position preceding the modified position to mark the position
49 // of deletion recovery.
50 prevPosition: FieldFocusRequest;
52 // Location of the cursor at time of initial action.
53 prevFocus: FieldFocusRequest;
57 export class MarcEditContext {
59 recordChange: EventEmitter<MarcRecord>;
60 fieldFocusRequest: EventEmitter<FieldFocusRequest>;
61 textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
62 recordType: 'biblio' | 'authority' = 'biblio';
64 lastFocused: FieldFocusRequest = null;
66 undoStack: UndoRedoAction[] = [];
67 redoStack: UndoRedoAction[] = [];
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;
76 private _record: MarcRecord;
77 set record(r: MarcRecord) {
78 if (r !== this._record) {
80 this._record.stampFieldIds();
81 this.recordChange.emit(r);
85 get record(): MarcRecord {
90 this.recordChange = new EventEmitter<MarcRecord>();
91 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
92 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
95 requestFieldFocus(req: FieldFocusRequest) {
96 // timeout allows for new components to be built before the
97 // focus request is emitted.
99 setTimeout(() => this.fieldFocusRequest.emit(req));
109 let remaining = null;
112 const action = this.undoStack.shift();
113 if (!action) { return; }
115 if (remaining === null) {
116 remaining = action.groupSize || 1;
120 action.isRedo = false;
121 this.distributeUndoRedo(action);
123 } while (remaining > 0);
127 let remaining = null;
130 const action = this.redoStack.shift();
131 if (!action) { return; }
133 if (remaining === null) {
134 remaining = action.groupSize || 1;
138 action.isRedo = true;
139 this.distributeUndoRedo(action);
141 } while (remaining > 0);
144 // Calculate stack action count taking groupSize (atomic action
145 // sets) into consideration.
146 stackCount(stack: UndoRedoAction[]): number {
150 stack.forEach(action => {
151 if (action.groupSize > 1) {
152 if (skip) { return; }
163 undoCount(): number {
164 return this.stackCount(this.undoStack);
167 redoCount(): number {
168 return this.stackCount(this.redoStack);
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;
181 distributeUndoRedo(action: UndoRedoAction) {
182 if (action instanceof TextUndoRedoAction) {
183 // Let the editable content component handle it.
184 this.textUndoRedoRequest.emit(action);
186 // Manage structural changes within
187 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
191 addToUndoStack(action: UndoRedoAction) {
192 this.changesPending = true;
193 this.undoStack.unshift(action);
196 handleStructuralUndoRedo(action: StructUndoRedoAction) {
198 if (action.wasAddition) {
199 // Remove the added field
201 if (action.subfield) {
202 const prevPos = action.subfield[2] - 1;
203 action.field.deleteExactSubfields(action.subfield);
204 this.focusSubfield(action.field, prevPos);
207 this.record.deleteFields(action.field);
210 // When deleting chunks, always return focus to the
211 // pre-insert position.
212 this.requestFieldFocus(action.prevFocus);
215 // Re-insert the removed field and focus it.
217 if (action.subfield) {
219 this.insertSubfield(action.field, action.subfield, true);
220 this.focusSubfield(action.field, action.subfield[2]);
224 const fieldId = action.position.fieldId;
226 this.record.getField(action.prevPosition.fieldId);
228 this.record.insertFieldsAfter(prevField, action.field);
230 // Recover the original fieldId, which gets re-stamped
231 // in this.record.insertFields* calls.
232 action.field.fieldId = fieldId;
234 // Focus the newly recovered field.
235 this.requestFieldFocus(action.position);
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;
247 action.wasAddition = !action.wasAddition;
249 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
251 moveTo.unshift(action);
254 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
256 // Human-driven changes invalidate the redo stack.
259 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
261 let prevPos: FieldFocusRequest = null;
264 position.target = 'sfc';
265 position.sfOffset = subfield[2];
268 // No need to track the previous field for subfield mods.
270 const prevField = this.record.getPreviousField(field.fieldId);
272 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
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;
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;
287 this.addToUndoStack(action);
290 deleteField(field: MarcField) {
291 this.trackStructuralUndo(field, false);
293 if (!this.focusNextTag(field)) {
294 this.focusPreviousTag(field);
297 this.record.deleteFields(field);
300 add00X(tag: string) {
302 const field: MarcField =
303 this.record.newField({tag : tag, data : STUB_DATA_00X});
305 this.record.insertOrderedFields(field);
307 this.trackStructuralUndo(field, true);
309 this.focusTag(field);
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);
320 const field = this.record.newField({
321 tag : '008', data : this.record.generate008()});
323 this.record.insertOrderedFields(field);
325 this.trackStructuralUndo(field, true);
327 this.focusTag(field);
330 // Add stub field before or after the context field
331 insertStubField(field: MarcField, before?: boolean) {
333 const newField = this.record.newField(
334 {tag: '999', subfields: [[' ', '', 0]]});
336 this.insertField(field, newField, before);
339 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
342 this.record.insertFieldsBefore(contextField, newField);
343 this.focusPreviousTag(contextField);
346 this.record.insertFieldsAfter(contextField, newField);
347 this.focusNextTag(contextField);
350 this.trackStructuralUndo(newField, true);
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];
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]++; }});
366 field.subfields.splice(position, 0, subfield);
369 this.focusSubfield(field, position);
370 this.trackStructuralUndo(field, true, subfield);
374 insertStubSubfield(field: MarcField, position: number) {
375 const newSf: MarcSubfield = [' ', '', position];
376 this.insertSubfield(field, newSf);
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) {
383 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
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;
392 this.requestFieldFocus(focus);
395 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
396 const sfpos = subfield[2] - 1; // previous subfield
398 this.trackStructuralUndo(field, false, subfield);
400 field.deleteExactSubfields(subfield);
402 this.focusSubfield(field, sfpos);
405 focusTag(field: MarcField) {
406 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
409 // Returns true if the field has a next tag to focus
410 focusNextTag(field: MarcField) {
411 const nextField = this.record.getNextField(field.fieldId);
413 this.focusTag(nextField);
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);
423 this.focusTag(prevField);