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?
26 // Grouped actions are tracked as multiple undo / redo actions, but
27 // are done and un-done as a unit.
31 export class TextUndoRedoAction extends UndoRedoAction {
35 export class StructUndoRedoAction extends UndoRedoAction {
36 /* Add or remove a part of the record (field, subfield, etc.) */
38 // Does this action track an addition or deletion.
41 // Field to add/delete or field to modify for subfield adds/deletes
44 // If this is a subfield modification.
45 subfield: MarcSubfield;
47 // Position preceding the modified position to mark the position
48 // of deletion recovery.
49 prevPosition: FieldFocusRequest;
51 // Location of the cursor at time of initial action.
52 prevFocus: FieldFocusRequest;
56 export class MarcEditContext {
58 recordChange: EventEmitter<MarcRecord>;
59 fieldFocusRequest: EventEmitter<FieldFocusRequest>;
60 textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
61 recordType: 'biblio' | 'authority' = 'biblio';
63 lastFocused: FieldFocusRequest = null;
65 undoStack: UndoRedoAction[] = [];
66 redoStack: UndoRedoAction[] = [];
68 // True if any changes have been made.
69 // For the 'rich' editor, this is any un-do-able actions.
70 // For the text edtior it's any text change.
71 changesPending: boolean;
73 private _record: MarcRecord;
74 set record(r: MarcRecord) {
75 if (r !== this._record) {
77 this._record.stampFieldIds();
78 this.recordChange.emit(r);
82 get record(): MarcRecord {
87 this.recordChange = new EventEmitter<MarcRecord>();
88 this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
89 this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
92 requestFieldFocus(req: FieldFocusRequest) {
93 // timeout allows for new components to be built before the
94 // focus request is emitted.
96 setTimeout(() => this.fieldFocusRequest.emit(req));
106 let remaining = null;
109 const action = this.undoStack.shift();
110 if (!action) { return; }
112 if (remaining === null) {
113 remaining = action.groupSize || 1;
117 action.isRedo = false;
118 this.distributeUndoRedo(action);
120 } while (remaining > 0);
124 let remaining = null;
127 const action = this.redoStack.shift();
128 if (!action) { return; }
130 if (remaining === null) {
131 remaining = action.groupSize || 1;
135 action.isRedo = true;
136 this.distributeUndoRedo(action);
138 } while (remaining > 0);
141 // Calculate stack action count taking groupSize (atomic action
142 // sets) into consideration.
143 stackCount(stack: UndoRedoAction[]): number {
147 stack.forEach(action => {
148 if (action.groupSize > 1) {
149 if (skip) { return; }
160 undoCount(): number {
161 return this.stackCount(this.undoStack);
164 redoCount(): number {
165 return this.stackCount(this.redoStack);
168 // Stamp the most recent 'size' entries in the undo stack
169 // as being an atomic undo/redo set.
170 setUndoGroupSize(size: number) {
171 for (let idx = 0; idx < size; idx++) {
172 if (this.undoStack[idx]) {
173 this.undoStack[idx].groupSize = size;
178 distributeUndoRedo(action: UndoRedoAction) {
179 if (action instanceof TextUndoRedoAction) {
180 // Let the editable content component handle it.
181 this.textUndoRedoRequest.emit(action);
183 // Manage structural changes within
184 this.handleStructuralUndoRedo(action as StructUndoRedoAction);
188 addToUndoStack(action: UndoRedoAction) {
189 this.changesPending = true;
190 this.undoStack.unshift(action);
193 handleStructuralUndoRedo(action: StructUndoRedoAction) {
195 if (action.wasAddition) {
196 // Remove the added field
198 if (action.subfield) {
199 const prevPos = action.subfield[2] - 1;
200 action.field.deleteExactSubfields(action.subfield);
201 this.focusSubfield(action.field, prevPos);
204 this.record.deleteFields(action.field);
207 // When deleting chunks, always return focus to the
208 // pre-insert position.
209 this.requestFieldFocus(action.prevFocus);
212 // Re-insert the removed field and focus it.
214 if (action.subfield) {
216 this.insertSubfield(action.field, action.subfield, true);
217 this.focusSubfield(action.field, action.subfield[2]);
221 const fieldId = action.position.fieldId;
223 this.record.getField(action.prevPosition.fieldId);
225 this.record.insertFieldsAfter(prevField, action.field);
227 // Recover the original fieldId, which gets re-stamped
228 // in this.record.insertFields* calls.
229 action.field.fieldId = fieldId;
231 // Focus the newly recovered field.
232 this.requestFieldFocus(action.position);
235 // When inserting chunks, track the location where the
236 // insert was requested so we can return the cursor so we
237 // can return the cursor to the scene of the crime if the
238 // undo is re-done or vice versa. This is primarily useful
239 // when performing global inserts like add00X, which can be
240 // done without the 00X field itself having focus.
241 action.prevFocus = this.lastFocused;
244 action.wasAddition = !action.wasAddition;
246 const moveTo = action.isRedo ? this.undoStack : this.redoStack;
248 moveTo.unshift(action);
251 trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
253 // Human-driven changes invalidate the redo stack.
256 const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
258 let prevPos: FieldFocusRequest = null;
261 position.target = 'sfc';
262 position.sfOffset = subfield[2];
265 // No need to track the previous field for subfield mods.
267 const prevField = this.record.getPreviousField(field.fieldId);
269 prevPos = {fieldId: prevField.fieldId, target: 'tag'};
273 const action = new StructUndoRedoAction();
274 action.field = field;
275 action.subfield = subfield;
276 action.wasAddition = isAddition;
277 action.position = position;
278 action.prevPosition = prevPos;
280 // For bulk adds (e.g. add a whole row) the field focused at
281 // time of action will be different than the added field.
282 action.prevFocus = this.lastFocused;
284 this.addToUndoStack(action);
287 deleteField(field: MarcField) {
288 this.trackStructuralUndo(field, false);
290 if (!this.focusNextTag(field)) {
291 this.focusPreviousTag(field);
294 this.record.deleteFields(field);
297 add00X(tag: string) {
299 const field: MarcField =
300 this.record.newField({tag : tag, data : STUB_DATA_00X});
302 this.record.insertOrderedFields(field);
304 this.trackStructuralUndo(field, true);
306 this.focusTag(field);
311 // delete all of the 008s
312 [].concat(this.record.field('008', true)).forEach(f => {
313 this.trackStructuralUndo(f, false);
314 this.record.deleteFields(f);
317 const field = this.record.newField({
318 tag : '008', data : this.record.generate008()});
320 this.record.insertOrderedFields(field);
322 this.trackStructuralUndo(field, true);
324 this.focusTag(field);
327 // Add stub field before or after the context field
328 insertStubField(field: MarcField, before?: boolean) {
330 const newField = this.record.newField(
331 {tag: '999', subfields: [[' ', '', 0]]});
333 this.insertField(field, newField, before);
336 insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
339 this.record.insertFieldsBefore(contextField, newField);
340 this.focusPreviousTag(contextField);
343 this.record.insertFieldsAfter(contextField, newField);
344 this.focusNextTag(contextField);
347 this.trackStructuralUndo(newField, true);
350 // Adds a new empty subfield to the provided field at the
351 // requested subfield position
352 insertSubfield(field: MarcField,
353 subfield: MarcSubfield, skipTracking?: boolean) {
354 const position = subfield[2];
356 // array index 3 contains that position of the subfield
357 // in the MARC field. When splicing a new subfield into
358 // the set, be sure the any that come after the new one
359 // have their positions bumped to reflect the shift.
360 field.subfields.forEach(
361 sf => {if (sf[2] >= position) { sf[2]++; }});
363 field.subfields.splice(position, 0, subfield);
366 this.focusSubfield(field, position);
367 this.trackStructuralUndo(field, true, subfield);
371 insertStubSubfield(field: MarcField, position: number) {
372 const newSf: MarcSubfield = [' ', '', position];
373 this.insertSubfield(field, newSf);
376 // Focus the requested subfield by its position. If its
377 // position is less than zero, focus the field's tag instead.
378 focusSubfield(field: MarcField, position: number) {
380 const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
383 // Focus the code instead of the value, because attempting to
384 // focus an empty (editable) div results in nothing getting focus.
385 focus.target = 'sfc';
386 focus.sfOffset = position;
389 this.requestFieldFocus(focus);
392 deleteSubfield(field: MarcField, subfield: MarcSubfield) {
393 const sfpos = subfield[2] - 1; // previous subfield
395 this.trackStructuralUndo(field, false, subfield);
397 field.deleteExactSubfields(subfield);
399 this.focusSubfield(field, sfpos);
402 focusTag(field: MarcField) {
403 this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
406 // Returns true if the field has a next tag to focus
407 focusNextTag(field: MarcField) {
408 const nextField = this.record.getNextField(field.fieldId);
410 this.focusTag(nextField);
416 // Returns true if the field has a previous tag to focus
417 focusPreviousTag(field: MarcField): boolean {
418 const prevField = this.record.getPreviousField(field.fieldId);
420 this.focusTag(prevField);