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 // True if any changes have been made.
65 // For the 'rich' editor, this is any un-do-able actions.
66 // For the text edtior it's any text change.
67 changesPending: boolean;
69 private _record: MarcRecord;
70 set record(r: MarcRecord) {
71 if (r !== this._record) {
73 this._record.stampFieldIds();
74 this.recordChange.emit(r);
78 get record(): MarcRecord {
83 this.recordChange = new EventEmitter<MarcRecord>();
84 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
85 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
88 requestFieldFocus(req: FieldFocusRequest) {
89 // timeout allows for new components to be built before the
90 // focus request is emitted.
91 setTimeout(() => this.fieldFocusRequest.emit(req));
100 const undo = this.undoStack.shift();
103 this.distributeUndoRedo(undo);
108 const redo = this.redoStack.shift();
111 this.distributeUndoRedo(redo);
115 distributeUndoRedo(action: UndoRedoAction) {
116 if (action instanceof TextUndoRedoAction) {
117 // Let the editable content component handle it.
118 this.textUndoRedoRequest.emit(action);
120 // Manage structural changes within
121 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
125 addToUndoStack(action: UndoRedoAction) {
126 this.changesPending = true;
127 this.undoStack.unshift(action);
130 handleStructuralUndoRedo(action: StructUndoRedoAction) {
132 if (action.wasAddition) {
133 // Remove the added field
135 if (action.subfield) {
136 const prevPos = action.subfield[2] - 1;
137 action.field.deleteExactSubfields(action.subfield);
138 this.focusSubfield(action.field, prevPos);
141 this.record.deleteFields(action.field);
144 // When deleting chunks, always return focus to the
145 // pre-insert position.
146 this.requestFieldFocus(action.prevFocus);
149 // Re-insert the removed field and focus it.
151 if (action.subfield) {
153 this.insertSubfield(action.field, action.subfield, true);
154 this.focusSubfield(action.field, action.subfield[2]);
158 const fieldId = action.position.fieldId;
160 this.record.getField(action.prevPosition.fieldId);
162 this.record.insertFieldsAfter(prevField, action.field);
164 // Recover the original fieldId, which gets re-stamped
165 // in this.record.insertFields* calls.
166 action.field.fieldId = fieldId;
168 // Focus the newly recovered field.
169 this.requestFieldFocus(action.position);
172 // When inserting chunks, track the location where the
173 // insert was requested so we can return the cursor so we
174 // can return the cursor to the scene of the crime if the
175 // undo is re-done or vice versa. This is primarily useful
176 // when performing global inserts like add00X, which can be
177 // done without the 00X field itself having focus.
178 action.prevFocus = this.lastFocused;
181 action.wasAddition = !action.wasAddition;
183 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
185 moveTo.unshift(action);
188 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
190 // Human-driven changes invalidate the redo stack.
193 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
195 let prevPos: FieldFocusRequest = null;
198 position.target = 'sfc';
199 position.sfOffset = subfield[2];
202 // No need to track the previous field for subfield mods.
204 const prevField = this.record.getPreviousField(field.fieldId);
206 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
210 const action = new StructUndoRedoAction();
211 action.field = field;
212 action.subfield = subfield;
213 action.wasAddition = isAddition;
214 action.position = position;
215 action.prevPosition = prevPos;
217 // For bulk adds (e.g. add a whole row) the field focused at
218 // time of action will be different than the added field.
219 action.prevFocus = this.lastFocused;
221 this.addToUndoStack(action);
224 deleteField(field: MarcField) {
225 this.trackStructuralUndo(field, false);
227 if (!this.focusNextTag(field)) {
228 this.focusPreviousTag(field);
231 this.record.deleteFields(field);
234 add00X(tag: string) {
236 const field: MarcField =
237 this.record.newField({tag : tag, data : STUB_DATA_00X});
239 this.record.insertOrderedFields(field);
241 this.trackStructuralUndo(field, true);
243 this.focusTag(field);
248 // delete all of the 008s
249 [].concat(this.record.field('008', true)).forEach(f => {
250 this.trackStructuralUndo(f, false);
251 this.record.deleteFields(f);
254 const field = this.record.newField({
255 tag : '008', data : this.record.generate008()});
257 this.record.insertOrderedFields(field);
259 this.trackStructuralUndo(field, true);
261 this.focusTag(field);
264 // Add stub field before or after the context field
265 insertStubField(field: MarcField, before?: boolean) {
267 const newField = this.record.newField(
268 {tag: '999', subfields: [[' ', '', 0]]});
270 this.insertField(field, newField, before);
273 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
276 this.record.insertFieldsBefore(contextField, newField);
277 this.focusPreviousTag(contextField);
280 this.record.insertFieldsAfter(contextField, newField);
281 this.focusNextTag(contextField);
284 this.trackStructuralUndo(newField, true);
287 // Adds a new empty subfield to the provided field at the
288 // requested subfield position
289 insertSubfield(field: MarcField,
290 subfield: MarcSubfield, skipTracking?: boolean) {
291 const position = subfield[2];
293 // array index 3 contains that position of the subfield
294 // in the MARC field. When splicing a new subfield into
295 // the set, be sure the any that come after the new one
296 // have their positions bumped to reflect the shift.
297 field.subfields.forEach(
298 sf => {if (sf[2] >= position) { sf[2]++; }});
300 field.subfields.splice(position, 0, subfield);
303 this.focusSubfield(field, position);
304 this.trackStructuralUndo(field, true, subfield);
308 insertStubSubfield(field: MarcField, position: number) {
309 const newSf: MarcSubfield = [' ', '', position];
310 this.insertSubfield(field, newSf);
313 // Focus the requested subfield by its position. If its
314 // position is less than zero, focus the field's tag instead.
315 focusSubfield(field: MarcField, position: number) {
317 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
320 // Focus the code instead of the value, because attempting to
321 // focus an empty (editable) div results in nothing getting focus.
322 focus.target = 'sfc';
323 focus.sfOffset = position;
326 this.requestFieldFocus(focus);
329 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
330 const sfpos = subfield[2] - 1; // previous subfield
332 this.trackStructuralUndo(field, false, subfield);
334 field.deleteExactSubfields(subfield);
336 this.focusSubfield(field, sfpos);
339 focusTag(field: MarcField) {
340 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
343 // Returns true if the field has a next tag to focus
344 focusNextTag(field: MarcField) {
345 const nextField = this.record.getNextField(field.fieldId);
347 this.focusTag(nextField);
353 // Returns true if the field has a previous tag to focus
354 focusPreviousTag(field: MarcField): boolean {
355 const prevField = this.record.getPreviousField(field.fieldId);
357 this.focusTag(prevField);