1 import {EventEmitter} from '@angular/core';
2 import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
3 import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
5 /* Per-instance MARC editor context. */
7 const STUB_DATA_00X = ' ';
9 export type MARC_EDITABLE_FIELD_TYPE =
10 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
12 export interface FieldFocusRequest {
14 target: MARC_EDITABLE_FIELD_TYPE;
15 sfOffset?: number; // focus a specific subfield by its offset
16 ffCode?: string; // fixed field code
19 export class UndoRedoAction {
20 // Which point in the record was modified.
21 position: FieldFocusRequest;
23 // Which stack do we toss this on once it's been applied?
27 export class TextUndoRedoAction extends UndoRedoAction {
31 export class StructUndoRedoAction extends UndoRedoAction {
32 /* Add or remove a part of the record (field, subfield, etc.) */
34 // Does this action track an addition or deletion.
37 // Field to add/delete or field to modify for subfield adds/deletes
40 // If this is a subfield modification.
41 subfield: MarcSubfield;
43 // Position preceding the modified position to mark the position
44 // of deletion recovery.
45 prevPosition: FieldFocusRequest;
47 // Location of the cursor at time of initial action.
48 prevFocus: FieldFocusRequest;
52 export class MarcEditContext {
54 recordChange: EventEmitter<MarcRecord>;
55 fieldFocusRequest: EventEmitter<FieldFocusRequest>;
56 textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
57 recordType: 'biblio' | 'authority' = 'biblio';
59 lastFocused: FieldFocusRequest = null;
61 undoStack: UndoRedoAction[] = [];
62 redoStack: UndoRedoAction[] = [];
64 private _record: MarcRecord;
65 set record(r: MarcRecord) {
66 if (r !== this._record) {
68 this._record.stampFieldIds();
69 this.recordChange.emit(r);
73 get record(): MarcRecord {
78 this.recordChange = new EventEmitter<MarcRecord>();
79 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
80 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
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));
95 const undo = this.undoStack.shift();
98 this.distributeUndoRedo(undo);
103 const redo = this.redoStack.shift();
106 this.distributeUndoRedo(redo);
110 distributeUndoRedo(action: UndoRedoAction) {
111 if (action instanceof TextUndoRedoAction) {
112 // Let the editable content component handle it.
113 this.textUndoRedoRequest.emit(action);
115 // Manage structural changes within
116 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
120 handleStructuralUndoRedo(action: StructUndoRedoAction) {
122 if (action.wasAddition) {
123 // Remove the added field
125 if (action.subfield) {
126 const prevPos = action.subfield[2] - 1;
127 action.field.deleteExactSubfields(action.subfield);
128 this.focusSubfield(action.field, prevPos);
131 this.record.deleteFields(action.field);
134 // When deleting chunks, always return focus to the
135 // pre-insert position.
136 this.requestFieldFocus(action.prevFocus);
139 // Re-insert the removed field and focus it.
141 if (action.subfield) {
143 this.insertSubfield(action.field, action.subfield, true);
144 this.focusSubfield(action.field, action.subfield[2]);
148 const fieldId = action.position.fieldId;
150 this.record.getField(action.prevPosition.fieldId);
152 this.record.insertFieldsAfter(prevField, action.field);
154 // Recover the original fieldId, which gets re-stamped
155 // in this.record.insertFields* calls.
156 action.field.fieldId = fieldId;
158 // Focus the newly recovered field.
159 this.requestFieldFocus(action.position);
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;
171 action.wasAddition = !action.wasAddition;
173 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
175 moveTo.unshift(action);
178 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
180 // Human-driven changes invalidate the redo stack.
183 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
185 let prevPos: FieldFocusRequest = null;
188 position.target = 'sfc';
189 position.sfOffset = subfield[2];
192 // No need to track the previous field for subfield mods.
194 const prevField = this.record.getPreviousField(field.fieldId);
196 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
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;
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;
211 this.undoStack.unshift(action);
214 deleteField(field: MarcField) {
215 this.trackStructuralUndo(field, false);
217 if (!this.focusNextTag(field)) {
218 this.focusPreviousTag(field);
221 this.record.deleteFields(field);
224 add00X(tag: string) {
226 const field: MarcField =
227 this.record.newField({tag : tag, data : STUB_DATA_00X});
229 this.record.insertOrderedFields(field);
231 this.trackStructuralUndo(field, true);
233 this.focusTag(field);
238 // delete all of the 008s
239 [].concat(this.record.field('008', true)).forEach(f => {
240 this.trackStructuralUndo(f, false);
241 this.record.deleteFields(f);
244 const field = this.record.newField({
245 tag : '008', data : this.record.generate008()});
247 this.record.insertOrderedFields(field);
249 this.trackStructuralUndo(field, true);
251 this.focusTag(field);
254 // Add stub field before or after the context field
255 insertStubField(field: MarcField, before?: boolean) {
257 const newField = this.record.newField(
258 {tag: '999', subfields: [[' ', '', 0]]});
260 this.insertField(field, newField, before);
263 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
266 this.record.insertFieldsBefore(contextField, newField);
267 this.focusPreviousTag(contextField);
270 this.record.insertFieldsAfter(contextField, newField);
271 this.focusNextTag(contextField);
274 this.trackStructuralUndo(newField, true);
277 // Adds a new empty subfield to the provided field at the
278 // requested subfield position
279 insertSubfield(field: MarcField,
280 subfield: MarcSubfield, skipTracking?: boolean) {
281 const position = subfield[2];
283 // array index 3 contains that position of the subfield
284 // in the MARC field. When splicing a new subfield into
285 // the set, be sure the any that come after the new one
286 // have their positions bumped to reflect the shift.
287 field.subfields.forEach(
288 sf => {if (sf[2] >= position) { sf[2]++; }});
290 field.subfields.splice(position, 0, subfield);
293 this.focusSubfield(field, position);
294 this.trackStructuralUndo(field, true, subfield);
298 insertStubSubfield(field: MarcField, position: number) {
299 const newSf: MarcSubfield = [' ', '', position];
300 this.insertSubfield(field, newSf);
303 // Focus the requested subfield by its position. If its
304 // position is less than zero, focus the field's tag instead.
305 focusSubfield(field: MarcField, position: number) {
307 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
310 // Focus the code instead of the value, because attempting to
311 // focus an empty (editable) div results in nothing getting focus.
312 focus.target = 'sfc';
313 focus.sfOffset = position;
316 this.requestFieldFocus(focus);
319 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
320 const sfpos = subfield[2] - 1; // previous subfield
322 this.trackStructuralUndo(field, false, subfield);
324 field.deleteExactSubfields(subfield);
326 this.focusSubfield(field, sfpos);
329 focusTag(field: MarcField) {
330 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
333 // Returns true if the field has a next tag to focus
334 focusNextTag(field: MarcField) {
335 const nextField = this.record.getNextField(field.fieldId);
337 this.focusTag(nextField);
343 // Returns true if the field has a previous tag to focus
344 focusPreviousTag(field: MarcField): boolean {
345 const prevField = this.record.getPreviousField(field.fieldId);
347 this.focusTag(prevField);