@Input() allowFreeText = false;
+ @Input() inputSize: number = null;
+
// Add a 'required' attribute to the input
isRequired: boolean;
@Input() set required(r: boolean) {
import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
import {DateRangeSelectComponent} from '@eg/share/daterange-select/daterange-select.component';
import {DateTimeSelectComponent} from '@eg/share/datetime-select/datetime-select.component';
+import {ContextMenuModule} from '@eg/share/context-menu/context-menu.module';
@NgModule({
DateSelectComponent,
OrgSelectComponent,
DateRangeSelectComponent,
- DateTimeSelectComponent,
+ DateTimeSelectComponent
],
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
NgbModule,
- EgCoreModule
+ EgCoreModule,
+ ContextMenuModule
],
exports: [
CommonModule,
OrgSelectComponent,
DateRangeSelectComponent,
DateTimeSelectComponent,
- ],
+ ContextMenuModule
+ ]
})
export class CommonWidgetsModule { }
--- /dev/null
+
+.eg-context-menu {
+ /* These fonts were applied specifically for the MARC editor
+ * context menus. Might want to make these optional. */
+ font-family: 'Lucida Console', Monaco, monospace;
+
+ /* put a hard limit on the popover width */
+ max-width: 550px;
+}
+
+.eg-context-menu .popover-body {
+ max-height: 400px;
+
+ /* Text exceeding the max-height / max-width will results in scrolls.
+ * In most cases, this should not happen. */
+ overflow-y: auto;
+ overflow-x: auto;
+}
+
+.eg-context-menu .popover-body .menu-entry {
+ /* force the menu to expand horizontally to display the text */
+ white-space: nowrap;
+}
+
+.eg-context-menu .popover-body .menu-entry:hover {
+ background-color: #f8f9fa;
+}
--- /dev/null
+
+<ng-template #menuTemplate>
+ <!-- apply (click) to div so user can click anywhere in the row -->
+ <div *ngFor="let entry of menuEntries; first as isFirst"
+ (click)="entryClicked(entry)" class="menu-entry {{entryClasses}}">
+ <a>{{entry.label}}</a>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, Output, EventEmitter, OnInit, ViewChild,
+ AfterViewInit, TemplateRef, ViewEncapsulation} from '@angular/core';
+import {ContextMenuService, ContextMenu, ContextMenuEntry} from './context-menu.service';
+
+@Component({
+ selector: 'eg-context-menu-container',
+ templateUrl: './context-menu-container.component.html',
+ styleUrls: ['context-menu-container.component.css'],
+ /* Our CSS affects the style of the popover, which may
+ * be beyond our reach for standard view encapsulation */
+ encapsulation: ViewEncapsulation.None
+})
+
+export class ContextMenuContainerComponent implements OnInit, AfterViewInit {
+
+ menuEntries: ContextMenuEntry[] = [];
+ @ViewChild('menuTemplate', {static: false}) menuTemplate: TemplateRef<any>;
+
+ constructor(private menuService: ContextMenuService) {}
+
+ ngOnInit() {
+
+ this.menuService.showMenuRequest.subscribe(
+ (menu: ContextMenu) => {
+
+ this.menuEntries = menu.entries
+ });
+ }
+
+ ngAfterViewInit() {
+ this.menuService.menuTemplate = this.menuTemplate;
+ }
+
+ entryClicked(entry: ContextMenuEntry) {
+ this.menuService.menuItemSelected.emit(entry);
+ }
+}
+
--- /dev/null
+import {Input, Output, EventEmitter, Directive} from '@angular/core';
+import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+import {ContextMenuService, ContextMenu, ContextMenuEntry} from './context-menu.service';
+
+
+/* Import all of this stuff so we can pass it to our parent
+ * class via its constructor */
+import {
+ Inject, Injector, Renderer2, ElementRef, TemplateRef, ViewContainerRef,
+ ComponentFactoryResolver, NgZone, ChangeDetectorRef, ApplicationRef
+} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {NgbPopoverConfig} from '@ng-bootstrap/ng-bootstrap';
+/* --- */
+
+@Directive({
+ selector: '[egContextMenu]',
+ exportAs: 'egContextMenu'
+})
+export class ContextMenuDirective extends NgbPopover {
+
+ triggers = 'contextmenu';
+ popoverClass = 'eg-context-menu';
+
+ menuEntries: ContextMenuEntry[] = [];
+ menu: ContextMenu;
+
+ @Input() set egContextMenu(menuEntries: ContextMenuEntry[]) {
+ this.menuEntries = menuEntries;
+ }
+
+ @Output() menuItemSelected: EventEmitter<ContextMenuEntry>;
+
+ // Only one active menu is allowed at a time.
+ static activeDirective: ContextMenuDirective;
+ static menuId = 0;
+
+ constructor(
+ p1: ElementRef<HTMLElement>, p2: Renderer2, p3: Injector,
+ p4: ComponentFactoryResolver, p5: ViewContainerRef, p6: NgbPopoverConfig,
+ p7: NgZone, @Inject(DOCUMENT) p8: any, p9: ChangeDetectorRef,
+ p10: ApplicationRef, private menuService: ContextMenuService) {
+
+ // relay injected services to parent
+ super(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
+
+ this.menuItemSelected = new EventEmitter<ContextMenuEntry>();
+
+ this.menuService.menuItemSelected.subscribe(
+ (entry: ContextMenuEntry) => {
+
+ // Only broadcast entry selection to my listeners if I'm
+ // hosting the menu where the selection occurred.
+
+ if (this.menu && this.menu.id === this.menuService.activeMenu.id) {
+ this.menuItemSelected.emit(entry);
+ }
+ });
+ }
+
+ open() {
+
+ // In certain scenarios (e.g. right-clicking on another context
+ // menu) an open popover will stay open. Force it closed here.
+ if (ContextMenuDirective.activeDirective) {
+ ContextMenuDirective.activeDirective.close();
+ ContextMenuDirective.activeDirective = null;
+ this.menuService.activeMenu == null;
+ }
+
+ if (!this.menuEntries ||
+ this.menuEntries.length === 0) {
+ return;
+ }
+
+ this.menu = new ContextMenu();
+ this.menu.id = ContextMenuDirective.menuId++;
+ this.menu.entries = this.menuEntries;
+
+ this.menuService.activeMenu = this.menu;
+ this.menuService.showMenuRequest.emit(this.menu);
+ this.ngbPopover = this.menuService.menuTemplate;
+
+ ContextMenuDirective.activeDirective = this;
+
+ super.open();
+ }
+}
+
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
+import {ContextMenuService} from './context-menu.service';
+import {ContextMenuDirective} from './context-menu.directive';
+import {ContextMenuContainerComponent} from './context-menu-container.component';
+
+@NgModule({
+ declarations: [
+ ContextMenuDirective,
+ ContextMenuContainerComponent
+ ],
+ imports: [
+ CommonModule,
+ NgbModule
+ ],
+ exports: [
+ ContextMenuDirective,
+ ContextMenuContainerComponent
+ ]
+})
+
+export class ContextMenuModule { }
+
--- /dev/null
+import {Injectable, EventEmitter, TemplateRef} from '@angular/core';
+import {tap} from 'rxjs/operators';
+
+/* Relay requests to/from the context menu directive and its
+ * template container component */
+
+export interface ContextMenuEntry {
+ value: string;
+ label: string;
+}
+
+export class ContextMenu {
+ id: number;
+ entries: ContextMenuEntry[];
+}
+
+@Injectable({providedIn: 'root'})
+export class ContextMenuService {
+
+ showMenuRequest: EventEmitter<ContextMenu>;
+ menuItemSelected: EventEmitter<ContextMenuEntry>;
+
+ menuTemplate: TemplateRef<any>;
+ activeMenu: ContextMenu;
+
+ constructor() {
+ this.showMenuRequest = new EventEmitter<ContextMenu>();
+ this.menuItemSelected = new EventEmitter<ContextMenuEntry>();
+ }
+}
+
+
--- /dev/null
+
+div[contenteditable] {
+ /* provide plenty of input space */
+ min-width: 2em;
+ /* match BS form-control border color */
+ border: 1px solid rgb(206, 212, 218);
+ /* match BS form-control input height */
+ min-height: calc(1.5em + .75rem + 2px);
+}
+
+.sf-delimiter {
+ /* match angjs color */
+ color: rgb(0, 0, 255)!important;
+ /* snuggle up to my subfield code */
+ margin-right: -0.5rem;
+}
+
+.sf-code {
+ /* match angjs color */
+ color: rgb(0, 0, 255)!important;
+}
+
--- /dev/null
+
+<ng-container *ngIf="bigText">
+ <div contenteditable
+ id='{{randId}}'
+ spellcheck="false"
+ class="d-inline-block text-dark text-break {{moreClasses}}"
+ [attr.tabindex]="fieldText ? -1 : ''"
+ [egContextMenu]="contextMenuEntries()"
+ (menuItemSelected)="contextMenuChange($event.value)"
+ (keydown)="inputKeyDown($event)"
+ (focus)="selectText()"
+ (blur)="inputBlurred()"
+ (input)="bigTextValueChange()">
+ </div>
+</ng-container>
+
+<ng-container *ngIf="!bigText">
+ <input
+ id='{{randId}}'
+ spellcheck="false"
+ class="text-dark rounded-0 form-control {{moreClasses}}"
+ [size]="inputSize()"
+ [maxlength]="maxLength || ''"
+ [disabled]="fieldText"
+ [attr.tabindex]="fieldText ? -1 : ''"
+ [egContextMenu]="contextMenuEntries()"
+ (menuItemSelected)="contextMenuChange($event.value)"
+ (keydown)="inputKeyDown($event)"
+ (focus)="selectText()"
+ (blur)="inputBlurred()"
+ [ngModel]="getContent()"
+ (ngModelChange)="setContent($event)"
+ />
+</ng-container>
+
--- /dev/null
+import {ElementRef, Component, Input, Output, OnInit, OnDestroy,
+ EventEmitter, AfterViewInit, Renderer2} from '@angular/core';
+import {Subscription} from 'rxjs';
+import {filter} from 'rxjs/operators';
+import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
+import {MarcEditContext, FieldFocusRequest, MARC_EDITABLE_FIELD_TYPE,
+ TextUndoRedoAction} from './editor-context';
+import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
+import {TagTableService} from './tagtable.service';
+
+/**
+ * MARC Editable Content Component
+ */
+
+@Component({
+ selector: 'eg-marc-editable-content',
+ templateUrl: './editable-content.component.html',
+ styleUrls: ['./editable-content.component.css']
+})
+
+export class EditableContentComponent
+ implements OnInit, AfterViewInit, OnDestroy {
+
+ @Input() context: MarcEditContext;
+ @Input() field: MarcField;
+ @Input() fieldType: MARC_EDITABLE_FIELD_TYPE = null;
+
+ // read-only field text. E.g. 'LDR'
+ @Input() fieldText: string = null;
+
+ // array of subfield code and subfield value
+ @Input() subfield: MarcSubfield;
+
+ @Input() fixedFieldCode: string;
+
+ // space-separated list of additional CSS classes to append
+ @Input() moreClasses: string;
+
+ get record(): MarcRecord { return this.context.record; }
+
+ bigText = false;
+ randId = Math.floor(Math.random() * 100000);
+ editInput: any; // <input/> or <div contenteditable/>
+ maxLength: number = null;
+
+ // Track the load-time content so we know what text value to
+ // track on our undo stack.
+ undoBackToText: string;
+
+ focusSub: Subscription;
+ undoRedoSub: Subscription;
+ isLeader: boolean; // convenience
+
+ // Cache of fixed field menu options
+ ffValues: ContextMenuEntry[] = [];
+
+ // Track the fixed field value locally since extracting the value
+ // in real time from the record, which adds padding to the text,
+ // causes usability problems.
+ ffValue: string;
+
+ constructor(
+ private renderer: Renderer2,
+ private tagTable: TagTableService) {}
+
+ ngOnInit() {
+ this.setupFieldType();
+ }
+
+ ngOnDestroy() {
+ if (this.focusSub) { this.focusSub.unsubscribe(); }
+ if (this.undoRedoSub) { this.undoRedoSub.unsubscribe(); }
+ }
+
+ watchForFocusRequests() {
+ this.focusSub = this.context.fieldFocusRequest.pipe(
+ filter((req: FieldFocusRequest) => this.focusRequestIsMe(req)))
+ .subscribe((req: FieldFocusRequest) => this.selectText(req));
+ }
+
+ watchForUndoRedoRequests() {
+ this.undoRedoSub = this.context.textUndoRedoRequest.pipe(
+ filter((action: TextUndoRedoAction) => this.focusRequestIsMe(action.position)))
+ .subscribe((action: TextUndoRedoAction) => this.processUndoRedo(action));
+ }
+
+ focusRequestIsMe(req: FieldFocusRequest): boolean {
+ if (req.target !== this.fieldType) { return false; }
+
+ if (this.field) {
+ if (req.fieldId !== this.field.fieldId) { return false; }
+ } else if (req.target === 'ldr') {
+ return this.isLeader;
+ } else if (req.target === 'ffld' &&
+ req.ffCode !== this.fixedFieldCode) {
+ return false;
+ }
+
+ if (req.sfOffset !== undefined &&
+ req.sfOffset !== this.subfield[2]) {
+ // this is not the subfield you are looking for.
+ return false;
+ }
+
+ return true;
+ }
+
+ selectText(req?: FieldFocusRequest) {
+ if (this.bigText) {
+ this.focusBigText();
+ } else {
+ this.editInput.select();
+ }
+
+ if (!req) {
+ // Focus request may have come from keyboard navigation,
+ // clicking, etc. Model the event as a focus request
+ // so it can be tracked the same.
+ req = {
+ fieldId: this.field ? this.field.fieldId : -1,
+ target: this.fieldType,
+ sfOffset: this.subfield ? this.subfield[2] : undefined,
+ ffCode: this.fixedFieldCode
+ };
+ }
+
+ this.context.lastFocused = req;
+ }
+
+ setupFieldType() {
+ const content = this.getContent();
+ this.undoBackToText = content;
+
+ switch (this.fieldType) {
+ case 'ldr':
+ this.isLeader = true;
+ if (content) { this.maxLength = content.length; }
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'tag':
+ this.maxLength = 3;
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'cfld':
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'ffld':
+ this.applyFFOptions();
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'ind1':
+ case 'ind2':
+ this.maxLength = 1;
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'sfc':
+ this.maxLength = 1;
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
+ case 'sfv':
+ this.bigText = true;
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+ }
+ }
+
+ applyFFOptions() {
+ return this.tagTable.getFfFieldMeta(
+ this.fixedFieldCode, this.record.recordType())
+ .then(fieldMeta => {
+ if (fieldMeta) {
+ this.maxLength = fieldMeta.length || 1;
+ }
+ });
+
+ // Fixed field options change when the record type changes.
+ this.context.recordChange.subscribe(_ => this.applyFFOptions());
+ }
+
+ // These are served dynamically to handle cases where a tag or
+ // subfield is modified in place.
+ contextMenuEntries(): ContextMenuEntry[] {
+ if (this.isLeader) { return; }
+
+ switch(this.fieldType) {
+ case 'tag':
+ return this.tagTable.getFieldTags();
+
+ case 'sfc':
+ return this.tagTable.getSubfieldCodes(this.field.tag);
+
+ case 'sfv':
+ return this.tagTable.getSubfieldValues(
+ this.field.tag, this.subfield[0]);
+
+ case 'ind1':
+ case 'ind2':
+ return this.tagTable.getIndicatorValues(
+ this.field.tag, this.fieldType);
+
+ case 'ffld':
+ return this.tagTable.getFfValues(
+ this.fixedFieldCode, this.record.recordType());
+ }
+
+ return null;
+ }
+
+ getContent(): string {
+ if (this.fieldText) { return this.fieldText; } // read-only
+
+ switch (this.fieldType) {
+ case 'ldr': return this.record.leader;
+ case 'cfld': return this.field.data;
+ case 'tag': return this.field.tag;
+ case 'sfc': return this.subfield[0];
+ case 'sfv': return this.subfield[1];
+ case 'ind1': return this.field.ind1;
+ case 'ind2': return this.field.ind2;
+
+ case 'ffld':
+ // When actively editing a fixed field, track its value
+ // in a local variable instead of pulling the value
+ // from record.extractFixedField(), which applies
+ // additional formattting, causing usability problems
+ // (e.g. unexpected spaces). Once focus is gone, the
+ // view will be updated with the correctly formatted
+ // value.
+
+ if ( this.ffValue === undefined ||
+ !this.context.lastFocused ||
+ !this.focusRequestIsMe(this.context.lastFocused)) {
+
+ this.ffValue =
+ this.record.extractFixedField(this.fixedFieldCode);
+ }
+ return this.ffValue;
+ }
+ return 'X';
+ }
+
+ setContent(value: string, propagatBigText?: boolean, skipUndoTrack?: boolean) {
+
+ if (this.fieldText) { return; } // read-only text
+
+ switch (this.fieldType) {
+ case 'ldr': this.record.leader = value; break;
+ case 'cfld': this.field.data = value; break;
+ case 'tag': this.field.tag = value; break;
+ case 'sfc': this.subfield[0] = value; break;
+ case 'sfv': this.subfield[1] = value; break;
+ case 'ind1': this.field.ind1 = value; break;
+ case 'ind2': this.field.ind2 = value; break;
+ case 'ffld':
+ // Track locally and propagate to the record.
+ this.ffValue = value;
+ this.record.setFixedField(this.fixedFieldCode, value);
+ break;
+ }
+
+ if (propagatBigText && this.bigText) {
+ // Propagate new content to the bigtext div.
+ // Should only be used when a content change occurrs via
+ // external means (i.e. not from a direct edit of the div).
+ this.editInput.innerText = value;
+ }
+
+ if (!skipUndoTrack) {
+ this.trackTextChangeForUndo(value);
+ }
+ }
+
+ trackTextChangeForUndo(value: string) {
+
+ // Human-driven changes invalidate the redo stack.
+ this.context.redoStack = [];
+
+ const lastUndo = this.context.undoStack[0];
+
+ if (lastUndo
+ && lastUndo instanceof TextUndoRedoAction
+ && lastUndo.textContent === this.undoBackToText
+ && this.focusRequestIsMe(lastUndo.position)) {
+ // Most recent undo entry was a text change event within the
+ // current atomic editing (focused) session for the input.
+ // Nothing else to track.
+ return;
+ }
+
+ const undo = new TextUndoRedoAction();
+ undo.position = this.context.lastFocused;
+ undo.textContent = this.undoBackToText;
+
+ this.context.undoStack.unshift(undo);
+ }
+
+ // Apply the undo or redo action and track its opposite
+ // action on the necessary stack
+ processUndoRedo(action: TextUndoRedoAction) {
+
+ // Undoing a text change
+ const recoverContent = this.getContent();
+ this.setContent(action.textContent, true, true);
+
+ action.textContent = recoverContent;
+ const moveTo = action.isRedo ?
+ this.context.undoStack : this.context.redoStack;
+
+ moveTo.unshift(action);
+ }
+
+ inputBlurred() {
+ // If the text content changed during this focus session,
+ // track the new value as the value the next session of
+ // text edits should return to upon undo.
+ this.undoBackToText = this.getContent();
+ }
+
+ // Propagate editable div content into our record
+ bigTextValueChange() {
+ this.setContent(this.editInput.innerText);
+ }
+
+ ngAfterViewInit() {
+ this.editInput = // numeric id requires [id=...] query selector
+ this.renderer.selectRootElement(`[id='${this.randId}']`);
+
+ // Initialize the editable div
+ this.editInput.innerText = this.getContent();
+ }
+
+ inputSize(): number {
+ if (this.maxLength) {
+ return this.maxLength + 1;
+ }
+ // give at least 2+ chars space and grow with the content
+ return Math.max(2, (this.getContent() || '').length) * 1.1;
+ }
+
+ focusBigText() {
+ const targetNode = this.editInput.firstChild;
+
+ if (!targetNode) {
+ // Div contains no text content, nothing to select
+ return;
+ }
+
+ const range = document.createRange();
+ range.setStart(targetNode, 0);
+ range.setEnd(targetNode, targetNode.length);
+
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+ }
+
+ // Route keydown events to the appropriate handler
+ inputKeyDown(evt: KeyboardEvent) {
+
+ switch(evt.key) {
+ case 'y':
+ if (evt.ctrlKey) { // redo
+ this.context.requestRedo();
+ evt.preventDefault();
+ }
+ return;
+
+ case 'z':
+ if (evt.ctrlKey) { // undo
+ this.context.requestUndo();
+ evt.preventDefault();
+ }
+ return;
+
+ case 'F6':
+ if (evt.shiftKey) {
+ // shift+F6 => add 006
+ this.context.add00X('006');
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ return;
+
+ case 'F7':
+ if (evt.shiftKey) {
+ // shift+F7 => add 007
+ this.context.add00X('007');
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ return;
+
+ case 'F8':
+ if (evt.shiftKey) {
+ // shift+F8 => add/replace 008
+ this.context.insertReplace008();
+ evt.preventDefault();
+ evt.stopPropagation();
+ }
+ return;
+ }
+
+ // None of the remaining key combos are supported by the LDR
+ // or fixed field editor.
+ if (this.fieldType === 'ldr' || this.fieldType === 'ffld') { return; }
+
+ switch (evt.key) {
+
+ case 'Enter':
+ if (evt.ctrlKey) {
+ // ctrl+enter == insert stub field after focused field
+ // ctrl+shift+enter == insert stub field before focused field
+ this.context.insertStubField(this.field, evt.shiftKey);
+ }
+
+ evt.preventDefault(); // Bare newlines not allowed.
+ break;
+
+ case 'Delete':
+
+ if (evt.ctrlKey) {
+ // ctrl+delete == delete whole field
+ this.context.deleteField(this.field);
+ evt.preventDefault();
+
+ } else if (evt.shiftKey && this.subfield) {
+ // shift+delete == delete subfield
+
+ this.context.deleteSubfield(this.field, this.subfield);
+ evt.preventDefault();
+ }
+
+ break;
+
+ case 'ArrowDown':
+
+ if (evt.ctrlKey) {
+ // ctrl+down == copy current field down one
+ this.context.insertField(
+ this.field, this.record.cloneField(this.field));
+ } else {
+ // avoid dupe focus requests
+ this.context.focusNextTag(this.field);
+ }
+
+ evt.preventDefault();
+ break;
+
+ case 'ArrowUp':
+
+ if (evt.ctrlKey) {
+ // ctrl+up == copy current field up one
+ this.context.insertField(
+ this.field, this.record.cloneField(this.field), true);
+ } else {
+ // avoid dupe focus requests
+ this.context.focusPreviousTag(this.field);
+ }
+
+ // up == move focus to tag of previous field
+ evt.preventDefault();
+ break;
+
+ case 'd': // thunk
+ case 'i':
+ if (evt.ctrlKey) {
+ // ctrl+i / ctrl+d == insert subfield
+ const pos = this.subfield ? this.subfield[2] + 1 : 0;
+ this.context.insertStubSubfield(this.field, pos);
+ evt.preventDefault();
+ }
+ break;
+ }
+ }
+
+ insertField(before: boolean) {
+
+ const newField = this.record.newField(
+ {tag: '999', subfields: [[' ', '', 0]]});
+
+ if (before) {
+ this.record.insertFieldsBefore(this.field, newField);
+ } else {
+ this.record.insertFieldsAfter(this.field, newField);
+ }
+
+ this.context.requestFieldFocus(
+ {fieldId: newField.fieldId, target: 'tag'});
+ }
+
+ deleteField() {
+ this.context.focusNextTag(this.field) ||
+ this.context.focusPreviousTag(this.field);
+
+ this.record.deleteFields(this.field);
+ }
+
+ deleteSubfield() {
+ // If subfields remain, focus the previous subfield.
+ // otherwise focus our tag.
+ const sfpos = this.subfield[2] - 1;
+
+ this.field.deleteExactSubfields(this.subfield);
+
+ const focus: FieldFocusRequest =
+ {fieldId: this.field.fieldId, target: 'tag'};
+
+ if (sfpos >= 0) {
+ focus.target = 'sfv';
+ focus.sfOffset = sfpos;
+ }
+
+ this.context.requestFieldFocus(focus);
+ }
+
+ contextMenuChange(value: string) {
+ this.setContent(value, true);
+ }
+}
+
+
--- /dev/null
+import {EventEmitter} from '@angular/core';
+import {MarcRecord, MarcField, MarcSubfield} from './marcrecord';
+import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+
+/* Per-instance MARC editor context. */
+
+const STUB_DATA_00X = ' ';
+
+export type MARC_EDITABLE_FIELD_TYPE =
+ 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv' | 'ffld';
+
+export interface FieldFocusRequest {
+ fieldId: number;
+ target: MARC_EDITABLE_FIELD_TYPE;
+ sfOffset?: number; // focus a specific subfield by its offset
+ ffCode?: string; // fixed field code
+}
+
+export class UndoRedoAction {
+ // Which point in the record was modified.
+ position: FieldFocusRequest;
+
+ // Which stack do we toss this on once it's been applied?
+ isRedo: boolean;
+}
+
+export class TextUndoRedoAction extends UndoRedoAction {
+ textContent: string;
+}
+
+export class StructUndoRedoAction extends UndoRedoAction {
+ /* Add or remove a part of the record (field, subfield, etc.) */
+
+ // Does this action track an addition or deletion.
+ wasAddition: boolean;
+
+ // Field to add/delete or field to modify for subfield adds/deletes
+ field: MarcField;
+
+ // If this is a subfield modification.
+ subfield: MarcSubfield;
+
+ // Position preceding the modified position to mark the position
+ // of deletion recovery.
+ prevPosition: FieldFocusRequest;
+
+ // Location of the cursor at time of initial action.
+ prevFocus: FieldFocusRequest;
+}
+
+
+export class MarcEditContext {
+
+ recordChange: EventEmitter<MarcRecord>;
+ fieldFocusRequest: EventEmitter<FieldFocusRequest>;
+ textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
+ recordType: 'biblio' | 'authority' = 'biblio';
+
+ lastFocused: FieldFocusRequest = null;
+
+ undoStack: UndoRedoAction[] = [];
+ redoStack: UndoRedoAction[] = [];
+
+ private _record: MarcRecord;
+ set record(r: MarcRecord) {
+ if (r !== this._record) {
+ this._record = r;
+ this._record.stampFieldIds();
+ this.recordChange.emit(r);
+ }
+ }
+
+ get record(): MarcRecord {
+ return this._record;
+ }
+
+ constructor() {
+ this.recordChange = new EventEmitter<MarcRecord>();
+ this.fieldFocusRequest = new EventEmitter<FieldFocusRequest>();
+ this.textUndoRedoRequest = new EventEmitter<TextUndoRedoAction>();
+ }
+
+ requestFieldFocus(req: FieldFocusRequest) {
+ // timeout allows for new components to be built before the
+ // focus request is emitted.
+ setTimeout(() => this.fieldFocusRequest.emit(req));
+ }
+
+ resetUndos() {
+ this.undoStack = [];
+ this.redoStack = [];
+ }
+
+ requestUndo() {
+ const undo = this.undoStack.shift();
+ if (undo) {
+ undo.isRedo = false;
+ this.distributeUndoRedo(undo);
+ }
+ }
+
+ requestRedo() {
+ const redo = this.redoStack.shift();
+ if (redo) {
+ redo.isRedo = true;
+ this.distributeUndoRedo(redo);
+ }
+ }
+
+ distributeUndoRedo(action: UndoRedoAction) {
+ if (action instanceof TextUndoRedoAction) {
+ // Let the editable content component handle it.
+ this.textUndoRedoRequest.emit(action);
+ } else {
+ // Manage structural changes within
+ this.handleStructuralUndoRedo(action as StructUndoRedoAction);
+ }
+ }
+
+ handleStructuralUndoRedo(action: StructUndoRedoAction) {
+
+ if (action.wasAddition) {
+ // Remove the added field
+
+ if (action.subfield) {
+ const prevPos = action.subfield[2] - 1;
+ action.field.deleteExactSubfields(action.subfield);
+ this.focusSubfield(action.field, prevPos);
+
+ } else {
+ this.record.deleteFields(action.field);
+ }
+
+ // When deleting chunks, always return focus to the
+ // pre-insert position.
+ this.requestFieldFocus(action.prevFocus);
+
+ } else {
+ // Re-insert the removed field and focus it.
+
+ if (action.subfield) {
+
+ this.insertSubfield(action.field, action.subfield, true);
+ this.focusSubfield(action.field, action.subfield[2]);
+
+ } else {
+
+ const fieldId = action.position.fieldId;
+ const prevField =
+ this.record.getField(action.prevPosition.fieldId);
+
+ this.record.insertFieldsAfter(prevField, action.field);
+
+ // Recover the original fieldId, which gets re-stamped
+ // in this.record.insertFields* calls.
+ action.field.fieldId = fieldId;
+
+ // Focus the newly recovered field.
+ this.requestFieldFocus(action.position);
+ }
+
+ // When inserting chunks, track the location where the
+ // insert was requested so we can return the cursor so we
+ // can return the cursor to the scene of the crime if the
+ // undo is re-done or vice versa. This is primarily useful
+ // when performing global inserts like add00X, which can be
+ // done without the 00X field itself having focus.
+ action.prevFocus = this.lastFocused;
+ }
+
+ action.wasAddition = !action.wasAddition;
+
+ const moveTo = action.isRedo ? this.undoStack : this.redoStack;
+
+ moveTo.unshift(action);
+ }
+
+ trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
+
+ // Human-driven changes invalidate the redo stack.
+ this.redoStack = [];
+
+ const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
+
+ let prevPos: FieldFocusRequest = null;
+
+ if (subfield) {
+ position.target = 'sfc';
+ position.sfOffset = subfield[2];
+
+ } else {
+ // No need to track the previous field for subfield mods.
+
+ const prevField = this.record.getPreviousField(field.fieldId);
+ if (prevField) {
+ prevPos = {fieldId: prevField.fieldId, target: 'tag'};
+ }
+ }
+
+ const action = new StructUndoRedoAction();
+ action.field = field;
+ action.subfield = subfield;
+ action.wasAddition = isAddition;
+ action.position = position;
+ action.prevPosition = prevPos;
+
+ // For bulk adds (e.g. add a whole row) the field focused at
+ // time of action will be different than the added field.
+ action.prevFocus = this.lastFocused;
+
+ this.undoStack.unshift(action);
+ }
+
+ deleteField(field: MarcField) {
+ this.trackStructuralUndo(field, false);
+
+ this.focusNextTag(field) || this.focusPreviousTag(field);
+
+ this.record.deleteFields(field);
+ }
+
+ add00X(tag: string) {
+
+ const field: MarcField =
+ this.record.newField({tag : tag, data : STUB_DATA_00X});
+
+ this.record.insertOrderedFields(field);
+
+ this.trackStructuralUndo(field, true);
+
+ this.focusTag(field);
+ }
+
+ insertReplace008() {
+
+ // delete all of the 008s
+ [].concat(this.record.field('008', true)).forEach(f => {
+ this.trackStructuralUndo(f, false);
+ this.record.deleteFields(f);
+ });
+
+ const field = this.record.newField({
+ tag : '008', data : this.record.generate008()});
+
+ this.record.insertOrderedFields(field);
+
+ this.trackStructuralUndo(field, true);
+
+ this.focusTag(field);
+ }
+
+ // Add stub field before or after the context field
+ insertStubField(field: MarcField, before?: boolean) {
+
+ const newField = this.record.newField(
+ {tag: '999', subfields: [[' ', '', 0]]});
+
+ this.insertField(field, newField, before);
+ }
+
+ insertField(contextField: MarcField, newField: MarcField, before?: boolean) {
+
+ if (before) {
+ this.record.insertFieldsBefore(contextField, newField);
+ this.focusPreviousTag(contextField);
+
+ } else {
+ this.record.insertFieldsAfter(contextField, newField);
+ this.focusNextTag(contextField);
+ }
+
+ this.trackStructuralUndo(newField, true);
+ }
+
+ // Adds a new empty subfield to the provided field at the
+ // requested subfield position
+ insertSubfield(field: MarcField,
+ subfield: MarcSubfield, skipTracking?: boolean) {
+ const position = subfield[2];
+
+ // array index 3 contains that position of the subfield
+ // in the MARC field. When splicing a new subfield into
+ // the set, be sure the any that come after the new one
+ // have their positions bumped to reflect the shift.
+ field.subfields.forEach(
+ sf => {if (sf[2] >= position) { sf[2]++; }});
+
+ field.subfields.splice(position, 0, subfield);
+
+ if (!skipTracking) {
+ this.focusSubfield(field, position);
+ this.trackStructuralUndo(field, true, subfield);
+ }
+ }
+
+ insertStubSubfield(field: MarcField, position: number) {
+ const newSf: MarcSubfield = [' ', '', position];
+ this.insertSubfield(field, newSf);
+ }
+
+ // Focus the requested subfield by its position. If its
+ // position is less than zero, focus the field's tag instead.
+ focusSubfield(field: MarcField, position: number) {
+
+ const focus: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
+
+ if (position >= 0) {
+ // Focus the code instead of the value, because attempting to
+ // focus an empty (editable) div results in nothing getting focus.
+ focus.target = 'sfc';
+ focus.sfOffset = position;
+ }
+
+ this.requestFieldFocus(focus);
+ }
+
+ deleteSubfield(field: MarcField, subfield: MarcSubfield) {
+ const sfpos = subfield[2] - 1; // previous subfield
+
+ this.trackStructuralUndo(field, false, subfield);
+
+ field.deleteExactSubfields(subfield);
+
+ this.focusSubfield(field, sfpos);
+ }
+
+ focusTag(field: MarcField) {
+ this.requestFieldFocus({fieldId: field.fieldId, target: 'tag'});
+ }
+
+ // Returns true if the field has a next tag to focus
+ focusNextTag(field: MarcField) {
+ const nextField = this.record.getNextField(field.fieldId);
+ if (nextField) {
+ this.focusTag(nextField);
+ return true;
+ }
+ return false;
+ }
+
+ // Returns true if the field has a previous tag to focus
+ focusPreviousTag(field: MarcField): boolean {
+ const prevField = this.record.getPreviousField(field.fieldId);
+ if (prevField) {
+ this.focusTag(prevField);
+ return true;
+ }
+ return false;
+ }
+}
+
<div class="row d-flex p-2 m-2">
<div class="flex-1"></div>
+
+ <h3 class="mr-2">
+ <span class="badge badge-light p-2" i18n>
+ Record Type {{record ? record.recordType() : ''}}
+ </span>
+ </h3>
+
<div class="mr-2">
<eg-combobox #sourceSelector
[entries]="sources"
<div class="row">
<div class="col-lg-12">
- <ngb-tabset [activeId]="editorTab">
- <ngb-tab title="Enhanced MARC Editor" i18n-title id="rich" *ngIf="!inPlaceMode">
+ <ngb-tabset [activeId]="editorTab" (tabChange)="tabChange($event)">
+ <ngb-tab title="Enhanced MARC Editor" i18n-title id="rich">
<ng-template ngbTabContent>
- <div class="alert alert-info mt-3" i18n>
- Enhanced MARC Editor is not yet implemented. See the
- <ng-container *ngIf="record && record.id">
- <a target="_blank"
- href="/eg/staff/cat/catalog/record/{{record.id}}/marc_edit">
- AngularJS MARC Editor.
- </a>
- </ng-container>
- <ng-container *ngIf="!record || !record.id">
- <a target="_blank" href="/eg/staff/cat/catalog/new_bib">
- AngularJS MARC Editor.
- </a>
+ <ng-container *ngIf="context && context.record">
+ <eg-marc-rich-editor [context]="context"></eg-marc-rich-editor>
</ng-container>
- </div>
</ng-template>
</ngb-tab>
<ngb-tab title="Flat Text Editor" i18n-title id="flat">
<ng-template ngbTabContent>
- <eg-marc-flat-editor></eg-marc-flat-editor>
+ <ng-container *ngIf="context && context.record">
+ <eg-marc-flat-editor [context]="context"></eg-marc-flat-editor>
+ </ng-container>
</ng-template>
</ngb-tab>
</ngb-tabset>
import {OrgService} from '@eg/core/org.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ToastService} from '@eg/share/toast/toast.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
import {StringComponent} from '@eg/share/string/string.component';
import {MarcRecord} from './marcrecord';
import {ComboboxEntry, ComboboxComponent
} from '@eg/share/combobox/combobox.component';
import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {MarcEditContext} from './editor-context';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
interface MarcSavedEvent {
marcXml: string;
export class MarcEditorComponent implements OnInit {
- record: MarcRecord;
editorTab: 'rich' | 'flat';
sources: ComboboxEntry[];
+ context: MarcEditContext;
+
+ @Input() recordType: 'biblio' | 'authority' = 'biblio';
@Input() set recordId(id: number) {
if (!id) { return; }
}
@Input() set recordXml(xml: string) {
- if (xml) { this.fromXml(xml); }
+ if (xml) {
+ this.fromXml(xml);
+ }
+ }
+
+ get record(): MarcRecord {
+ return this.context.record;
}
// Tell us which record source to select by default.
private auth: AuthService,
private org: OrgService,
private pcrud: PcrudService,
- private toast: ToastService
+ private toast: ToastService,
+ private store: ServerStoreService
) {
this.sources = [];
this.recordSaved = new EventEmitter<MarcSavedEvent>();
+ this.context = new MarcEditContext();
}
ngOnInit() {
- // Default to flat for now since it's all that's supported.
- this.editorTab = 'flat';
+
+ this.context.recordType = this.recordType;
+
+ this.store.getItem('cat.marcedit.flateditor').then(
+ useFlat => this.editorTab = useFlat ? 'flat' : 'rich');
this.pcrud.retrieveAll('cbs').subscribe(
src => this.sources.push({id: +src.id(), label: src.source()}),
);
}
+ // Remember the last used tab as the preferred tab.
+ tabChange(evt: NgbTabChangeEvent) {
+
+ // Avoid undo persistence across tabs since that could result
+ // in changes getting lost.
+ this.context.resetUndos();
+
+ if (evt.nextId === 'flat') {
+ this.store.setItem('cat.marcedit.flateditor', true);
+ } else {
+ this.store.removeItem('cat.marcedit.flateditor');
+ }
+ }
+
saveRecord(): Promise<any> {
const xml = this.record.toXml();
fromId(id: number): Promise<any> {
return this.pcrud.retrieve('bre', id)
.toPromise().then(bib => {
- this.record = new MarcRecord(bib.marc());
+ this.context.record = new MarcRecord(bib.marc());
this.record.id = id;
this.record.deleted = bib.deleted() === 't';
if (bib.source()) {
}
fromXml(xml: string) {
- this.record = new MarcRecord(xml);
+ this.context.record = new MarcRecord(xml);
this.record.id = null;
}
--- /dev/null
+
+:host >>> .popover {
+ font-family: 'Lucida Console', Monaco, monospace;
+ max-width: 550px;
+}
+
+:host >>> .popover-body {
+ max-height: 400px;
+ overflow-y: auto;
+ overflow-x: auto;
+}
+
+:host >>> .popover-body .menu-entry {
+ white-space: nowrap;
+}
+
+:host >>> .popover-body .menu-entry:hover {
+ background-color: #f8f9fa; /* bootstrap color */
+}
+
--- /dev/null
+
+<ng-container *ngIf="fieldMeta">
+
+ <div class="d-flex">
+ <div class="flex-4">
+ <span id='label-{{randId}}' class="text-left font-weight-bold">
+ {{fieldLabel}}
+ </span>
+ </div>
+ <div class="flex-5">
+ <eg-marc-editable-content [context]="context"
+ [fixedFieldCode]="fieldCode" fieldType="ffld" moreClasses="p-1">
+ </eg-marc-editable-content>
+ </div>
+ </div>
+</ng-container>
--- /dev/null
+import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {MarcRecord} from './marcrecord';
+import {MarcEditContext} from './editor-context';
+import {TagTableService} from './tagtable.service';
+
+/**
+ * MARC Fixed Field Editing Component
+ */
+
+@Component({
+ selector: 'eg-fixed-field',
+ templateUrl: './fixed-field.component.html',
+ styleUrls: ['fixed-field.component.css']
+})
+
+export class FixedFieldComponent implements OnInit {
+
+ @Input() fieldCode: string;
+ @Input() fieldLabel: string;
+ @Input() context: MarcEditContext;
+
+ get record(): MarcRecord { return this.context.record; }
+
+ fieldMeta: IdlObject;
+ randId = Math.floor(Math.random() * 10000000);
+
+ constructor(private tagTable: TagTableService) {}
+
+ ngOnInit() {
+ this.init().then(_ =>
+ this.context.recordChange.subscribe(__ => this.init()));
+ }
+
+ init(): Promise<any> {
+ if (!this.record) { return Promise.resolve(); }
+
+ // If no field metadata is found for this fixed field code and
+ // record type combo, the field will be hidden in the UI.
+ return this.tagTable.getFfFieldMeta(
+ this.fieldCode, this.record.recordType())
+ .then(fieldMeta => this.fieldMeta = fieldMeta);
+ }
+}
+
+
--- /dev/null
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Type" fieldLabel="Type"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="ELvl" fieldLabel="ELvl"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Source" fieldLabel="Source"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Audn" fieldLabel="Audn"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Ctrl" fieldLabel="Ctrl"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Lang" fieldLabel="Lang"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="BLvl" fieldLabel="BLvl"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Form" fieldLabel="Form"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Conf" fieldLabel="Conf"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Biog" fieldLabel="Biog"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="MRec" fieldLabel="MRec"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Ctry" fieldLabel="Ctry"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="s_l" fieldLabel="s_l"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Cont" fieldLabel="Cont"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="GPub" fieldLabel="GPub"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="LitF" fieldLabel="LitF"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Indx" fieldLabel="Indx"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Alph" fieldLabel="Alph"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Desc" fieldLabel="Desc"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Ills" fieldLabel="Ills"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Fest" fieldLabel="Fest"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="DtSt" fieldLabel="DtSt"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Date1" fieldLabel="Date1"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Date2" fieldLabel="Date2"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="SrTp" fieldLabel="SrTp"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Regl" fieldLabel="Regl"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Orig" fieldLabel="Orig"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Freq" fieldLabel="Freq"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="EntW" fieldLabel="EntW"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="TrAr" fieldLabel="TrAr"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Part" fieldLabel="Part"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="LTxt" fieldLabel="LTxt"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="FMus" fieldLabel="FMus"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="AccM" fieldLabel="AccM"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Comp" fieldLabel="Comp"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="SpFm" fieldLabel="SpFm"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Relf" fieldLabel="Relf"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Proj" fieldLabel="Proj"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="CrTp" fieldLabel="CrTp"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="TMat" fieldLabel="TMat"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Time" fieldLabel="Time"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Tech" fieldLabel="Tech"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="File" fieldLabel="File"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Type_tbmfhd" fieldLabel="Type_tbmfhd"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="ELvl_tbmfhd" fieldLabel="ELvl_tbmfhd"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Item_tbmfhd" fieldLabel="Item_tbmfhd"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="GeoDiv" fieldLabel="GeoDiv"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Roman" fieldLabel="Roman"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="CatLang" fieldLabel="CatLang"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Kind" fieldLabel="Kind"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Rules" fieldLabel="Rules"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Subj" fieldLabel="Subj"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Series" fieldLabel="Series"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="SerNum" fieldLabel="SerNum"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="NameUse" fieldLabel="NameUse"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="SubjUse" fieldLabel="SubjUse"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="SerUse" fieldLabel="SerUse"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="TypeSubd" fieldLabel="TypeSubd"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="GovtAgn" fieldLabel="GovtAgn"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="RefStatus" fieldLabel="RefStatus"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="UpdStatus" fieldLabel="UpdStatus"></eg-fixed-field>
+ </div>
+</div>
+<div class="row p-0">
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Name" fieldLabel="Name"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="Status" fieldLabel="Status"></eg-fixed-field>
+ </div>
+ <div class="col-lg-2">
+ <eg-fixed-field i18n-fieldLabel [context]="context"
+ fieldCode="ModRec" fieldLabel="ModRec"></eg-fixed-field>
+ </div>
+</div>
+
--- /dev/null
+import {Component, Input, Output, OnInit, AfterViewInit, EventEmitter,
+ OnDestroy} from '@angular/core';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {MarcRecord} from './marcrecord';
+import {MarcEditContext} from './editor-context';
+import {TagTableService} from './tagtable.service';
+
+/**
+ * MARC Fixed Fields Editor Component
+ */
+
+@Component({
+ selector: 'eg-fixed-fields-editor',
+ templateUrl: './fixed-fields-editor.component.html'
+})
+
+export class FixedFieldsEditorComponent implements OnInit {
+
+ @Input() context: MarcEditContext;
+ get record(): MarcRecord { return this.context.record; }
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private tagTable: TagTableService
+ ) {}
+
+ ngOnInit() {}
+}
+
-import {Component, Input, OnInit, Host} from '@angular/core';
+import {Component, Input, OnInit} from '@angular/core';
import {IdlService} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
import {ServerStoreService} from '@eg/core/server-store.service';
-import {MarcEditorComponent} from './editor.component';
import {MarcRecord} from './marcrecord';
+import {MarcEditContext} from './editor-context';
/**
* MARC Record flat text (marc-breaker) editor.
export class MarcFlatEditorComponent implements OnInit {
+ @Input() context: MarcEditContext;
get record(): MarcRecord {
- return this.editor.record;
+ return this.context.record;
}
constructor(
private idl: IdlService,
private org: OrgService,
- private store: ServerStoreService,
- @Host() private editor: MarcEditorComponent
- ) {
- }
+ private store: ServerStoreService
+ ) {}
- ngOnInit() {}
+ ngOnInit() {
+ // Be sure changes made in the enriched editor are
+ // reflected here.
+ this.record.breakerText = this.record.toBreaker();
+ }
// When we have breaker text, limit the vertical expansion of the
// text area to the size of the data plus a little padding.
import {NgModule} from '@angular/core';
import {StaffCommonModule} from '@eg/staff/common.module';
+import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
import {MarcEditorComponent} from './editor.component';
import {MarcRichEditorComponent} from './rich-editor.component';
import {MarcFlatEditorComponent} from './flat-editor.component';
+import {FixedFieldsEditorComponent} from './fixed-fields-editor.component';
+import {FixedFieldComponent} from './fixed-field.component';
+import {TagTableService} from './tagtable.service';
+import {EditableContentComponent} from './editable-content.component';
@NgModule({
declarations: [
MarcEditorComponent,
MarcRichEditorComponent,
- MarcFlatEditorComponent
+ MarcFlatEditorComponent,
+ FixedFieldsEditorComponent,
+ FixedFieldComponent,
+ EditableContentComponent
],
imports: [
- StaffCommonModule
+ StaffCommonModule,
+ CommonWidgetsModule
],
exports: [
MarcEditorComponent
],
providers: [
+ TagTableService
]
})
-/**
- * Simple wrapper class for our external MARC21.Record JS library.
- */
+import {EventEmitter} from '@angular/core';
+
+/* Wrapper class for our external MARC21.Record JS library. */
declare var MARC21;
// MARC breaker delimiter
const DELIMITER = '$';
+export interface MarcSubfield // code, value, position
+ extends Array<string|number>{0: string; 1: string; 2: number}
+
+// Only contains the attributes/methods we need so far.
+export interface MarcField {
+ fieldId?: number;
+ data?: string;
+ tag?: string;
+ ind1?: string;
+ ind2?: string;
+ subfields?: MarcSubfield[];
+
+ isControlfield(): boolean;
+
+ deleteExactSubfields(...subfield: MarcSubfield[]): number;
+}
+
export class MarcRecord {
id: number; // Database ID when known.
record: any; // MARC21.Record object
breakerText: string;
+ // Let clients know some fixed field shuffling may have occured.
+ // Emits the fixed field code.
+ fixedFieldChange: EventEmitter<string>;
+
+ get leader(): string {
+ return this.record.leader;
+ }
+
+ set leader(l: string) {
+ this.record.leader = l;
+ }
+
+ get fields(): MarcField[] {
+ return this.record.fields;
+ }
+
+ set fields(f: MarcField[]) {
+ this.record.fields = f;
+ }
+
constructor(xml: string) {
this.record = new MARC21.Record({marcxml: xml, delimiter: DELIMITER});
this.breakerText = this.record.toBreaker();
+ this.fixedFieldChange = new EventEmitter<string>();
}
toXml(): string {
return this.record.toBreaker();
}
+ recordType(): string {
+ return this.record.recordType();
+ }
+
absorbBreakerChanges() {
this.record = new MARC21.Record(
{marcbreaker: this.breakerText, delimiter: DELIMITER});
}
+
+ extractFixedField(fieldCode: string): string {
+ return this.record.extractFixedField(fieldCode);
+ }
+
+ setFixedField(fieldCode: string, fieldValue: string): string {
+ const response = this.record.setFixedField(fieldCode, fieldValue);
+ this.fixedFieldChange.emit(fieldCode);
+ return response;
+ }
+
+ // Give each field an identifier so it may be referenced later.
+ stampFieldIds() {
+ this.fields.forEach(f => this.stampFieldId(f));
+ }
+
+ stampFieldId(field: MarcField) {
+ if (!field.fieldId) {
+ field.fieldId = Math.floor(Math.random() * 10000000);
+ }
+ }
+
+ field(spec: string, wantArray?: boolean): MarcField | MarcField[] {
+ return this.record.field(spec, wantArray);
+ }
+
+ insertFieldsBefore(field: MarcField, ...newFields: MarcField[]) {
+ this.record.insertFieldsBefore.apply(
+ this.record, [field].concat(newFields));
+ this.stampFieldIds();
+ }
+
+ insertFieldsAfter(field: MarcField, ...newFields: MarcField[]) {
+ this.record.insertFieldsAfter.apply(
+ this.record, [field].concat(newFields));
+ this.stampFieldIds();
+ }
+
+ insertOrderedFields(...newFields: MarcField[]) {
+ this.record.insertOrderedFields.apply(this.record, newFields);
+ this.stampFieldIds();
+ }
+
+ generate008(): MarcField {
+ return this.record.generate008();
+ }
+
+
+ deleteFields(...fields: MarcField[]) {
+ this.record.deleteFields.apply(this.record, fields);
+ }
+
+ getField(id: number): MarcField {
+ return this.fields.filter(f => f.fieldId === id)[0];
+ }
+
+ getPreviousField(id: number): MarcField {
+ for (let idx = 0; idx < this.fields.length; idx++) {
+ if (this.fields[idx].fieldId === id) {
+ return this.fields[idx - 1];
+ }
+ }
+ }
+
+ getNextField(id: number): MarcField {
+ for (let idx = 0; idx < this.fields.length; idx++) {
+ if (this.fields[idx].fieldId === id) {
+ return this.fields[idx + 1];
+ }
+ }
+ }
+
+ // Turn an field-ish object into a proper MARC.Field
+ newField(props: any): MarcField {
+ const field = new MARC21.Field(props);
+ this.stampFieldId(field);
+ return field;
+ }
+
+ cloneField(field: any): MarcField {
+ const props: any = {tag: field.tag};
+
+ if (field.isControlfield()) {
+ props.data = field.data;
+
+ } else {
+ props.ind1 = field.ind1;
+ props.ind2 = field.ind2;
+ props.subfields = this.cloneSubfields(field.subfields);
+ }
+
+ return this.newField(props);
+ }
+
+ cloneSubfields(subfields: MarcSubfield[]): MarcSubfield[] {
+ const root = [];
+ subfields.forEach(sf => root.push([].concat(sf)));
+ return root;
+ }
}
+
+.fixed-fields-container {
+ /*
+ * wait for https://bugs.launchpad.net/evergreen/+bug/1735568 approval
+ background-color: lightcyan;
+ border-bottom: 1px solid gray;
+ */
+}
+
+<ng-container *ngIf="!dataLoaded">
+ <div class="row mt-5">
+ <div class="offset-lg-3 col-lg-6">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+ </div>
+</ng-container>
+
+<ng-container *ngIf="dataLoaded">
+ <div class="mt-3 text-monospace"
+ (contextmenu)="$event.preventDefault()">
+ <div class="row pb-2 mb-2 border-bottom border-muted">
+ <div class="col-lg-9 fixed-fields-container">
+ <eg-fixed-fields-editor [context]="context"></eg-fixed-fields-editor>
+ </div>
+ <div class="col-lg-3">
+ <div><button class="btn btn-outline-dark"
+ (click)="showHelp = !showHelp" i18n>Help</button></div>
+ <div class="mt-2"><button class="btn btn-outline-dark"
+ [disabled]="true"
+ (click)="validate()" i18n>Validate</button></div>
+ <div class="mt-2">
+ <button type="button" class="btn btn-outline-info"
+ [disabled]="undoCount() < 1" (click)="undo()">
+ Undo <span class="badge badge-info">{{undoCount()}}</span>
+ </button>
+ <button type="button" class="btn btn-outline-info ml-2"
+ [disabled]="redoCount() < 1" (click)="redo()">
+ Redo <span class="badge badge-info">{{redoCount()}}</span>
+ </button>
+ </div>
+ <div class="mt-2">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox"
+ [disabled]="true"
+ [(ngModel)]="stackSubfields" id="stack-subfields-{{randId}}">
+ <label class="form-check-label" for="stack-subfields-{{randId}}">
+ Stack Subfields
+ </label>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-1">
+ </div>
+ </div>
+ <div *ngIf="showHelp" class="row m-2">
+ <div class="col-lg-4">
+ <ul>
+ <li>Undo: CTRL-z</li>
+ <li>Redo: CTRL-y</li>
+ <li>Add Row: CTRL+Enter</li>
+ <li>Insert Row: CTRL+Shift+Enter</li>
+ </ul>
+ </div>
+ <div class="col-lg-4">
+ <ul>
+ <li>Copy Current Row Above: CTRL+Up</li>
+ <li>Copy Current Row Below: CTRL+Down</li>
+ <li>Add Subfield: CTRL+D or CTRL+I</li>
+ <li>Remove Row: CTRL+Del</li>
+ </ul>
+ </div>
+ <div class="col-lg-4">
+ <ul>
+ <li>Remove Subfield: Shift+Del</li>
+ <li>Create/Replace 006: Shift+F6</li>
+ <li>Create/Replace 007: Shift+F7</li>
+ <li>Create/Replace 008: Shift+F8</li>
+ </ul>
+ </div>
+ </div>
+
+ <!-- LEADER -->
+ <div class="row pt-0 pb-0 pl-3 form-horizontal">
+ <eg-marc-editable-content [context]="context" fieldType="tag"
+ fieldText="LDR" i18n-fieldText moreClasses="p-1">
+ </eg-marc-editable-content>
+
+ <eg-marc-editable-content [context]="context" fieldType="ldr"
+ moreClasses="p-1 pr-2">
+ </eg-marc-editable-content>
+ </div>
+
+ <!-- CONTROL FIELDS -->
+ <div class="row pt-0 pb-0 pl-3 form-horizontal"
+ *ngFor="let field of controlFields()">
+
+ <eg-marc-editable-content [context]="context" fieldType="tag"
+ [field]="field" moreClasses="p-1">
+ </eg-marc-editable-content>
+
+ <eg-marc-editable-content [context]="context" fieldType="cfld"
+ [field]="field" moreClasses="p-1">
+ </eg-marc-editable-content>
+ </div>
+
+ <!-- data fields -->
+ <div class="row pt-0 pb-0 pl-3 form-horizontal"
+ *ngFor="let field of dataFields()">
+
+ <!-- TAG -->
+ <eg-marc-editable-content [context]="context" fieldType="tag"
+ [field]="field" moreClasses="p-1">
+ </eg-marc-editable-content>
+
+ <!-- INDICATOR 1 -->
+ <eg-marc-editable-content [context]="context" fieldType="ind1"
+ [field]="field" moreClasses="p-1">
+ </eg-marc-editable-content>
+
+ <!-- INDICATOR 2 -->
+ <eg-marc-editable-content [context]="context" fieldType="ind2"
+ [field]="field" moreClasses="p-1">
+ </eg-marc-editable-content>
+
+ <!-- SUBFIELDS -->
+ <ng-container *ngFor="let subfield of field.subfields">
+
+ <!-- SUBFIELD DECORATOR/DELIMITER -->
+ <eg-marc-editable-content fieldText="‡" i18n-fieldText
+ moreClasses="sf-delimiter border-right-0 bg-transparent p-1 pr-0">
+ </eg-marc-editable-content>
+
+ <!-- SUBFIELD CHARACTER -->
+ <eg-marc-editable-content [context]="context" fieldType="sfc"
+ [field]="field" [subfield]="subfield"
+ moreClasses="sf-code border-left-0 p-1 pl-0">
+ </eg-marc-editable-content>
+
+ <!-- SUBFIELD VALUE -->
+ <eg-marc-editable-content [context]="context" fieldType="sfv"
+ [field]="field" [subfield]="subfield" moreClasses="p-1 pt-2">
+ </eg-marc-editable-content>
+ </ng-container>
+ </div>
+ </div>
+</ng-container>
+
OnDestroy} from '@angular/core';
import {IdlService} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import {TagTableService} from './tagtable.service';
+import {MarcRecord, MarcField} from './marcrecord';
+import {MarcEditContext} from './editor-context';
+
/**
* MARC Record rich editor interface.
export class MarcRichEditorComponent implements OnInit {
+ @Input() context: MarcEditContext;
+ get record(): MarcRecord { return this.context.record; }
+
+ dataLoaded: boolean;
+ showHelp: boolean;
+ randId = Math.floor(Math.random() * 100000);
+ stackSubfields: boolean;
+
constructor(
private idl: IdlService,
- private org: OrgService
- ) {
+ private org: OrgService,
+ private tagTable: TagTableService
+ ) {}
+
+ ngOnInit() {
+ this.init().then(_ =>
+ this.context.recordChange.subscribe(__ => this.init()));
+ }
+
+ init(): Promise<any> {
+ this.dataLoaded = false;
+
+ if (!this.record) { return Promise.resolve(); }
+
+ return Promise.all([
+ this.tagTable.loadTagTable({marcRecordType: this.context.recordType}),
+ this.tagTable.getFfPosTable(this.record.recordType()),
+ this.tagTable.getFfValueTable(this.record.recordType())
+ ]).then(_ => this.dataLoaded = true);
+ }
+
+ undoCount(): number {
+ return this.context.undoStack.length;
}
- ngOnInit() {}
+ redoCount(): number {
+ return this.context.redoStack.length;
+ }
+
+ undo() {
+ this.context.requestUndo();
+ }
+
+ redo() {
+ this.context.requestRedo();
+ }
+
+ controlFields(): MarcField[] {
+ return this.record.fields.filter(f => f.isControlfield());
+ }
+
+ dataFields(): MarcField[] {
+ return this.record.fields.filter(f => !f.isControlfield());
+ }
}
--- /dev/null
+import {Injectable, EventEmitter} from '@angular/core';
+import {map, tap} from 'rxjs/operators';
+import {StoreService} from '@eg/core/store.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {EventService} from '@eg/core/event.service';
+import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
+
+interface TagTableSelector {
+ marcFormat?: string;
+ marcRecordType?: string;
+}
+
+const defaultTagTableSelector: TagTableSelector = {
+ marcFormat : 'marc21',
+ marcRecordType : 'biblio'
+}
+
+@Injectable()
+export class TagTableService {
+
+ // Current set of tags in list and map form.
+ tagMap: {[tag: string]: any} = {};
+ ffPosMap: {[rtype: string]: any[]} = {};
+ ffValueMap: {[rtype: string]: any} = {};
+
+ extractedValuesCache:
+ {[valueType: string]: {[which: string]: any}} = {};
+
+ constructor(
+ private store: StoreService,
+ private auth: AuthService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private evt: EventService
+ ) {
+
+ this.extractedValuesCache = {
+ fieldtags: {},
+ indicators: {},
+ sfcodes: {},
+ sfvalues: {},
+ ffvalues: {}
+ };
+ }
+
+ // Various data needs munging for display. Cached the modified
+ // values since they are refernced repeatedly by the UI code.
+ fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
+ const part1 = this.extractedValuesCache[dataType][which];
+ if (which2) {
+ if (part1) {
+ return part1[which2];
+ }
+ } else {
+ return part1;
+ }
+ }
+
+ toCache(dataType: string, which: string,
+ which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
+ const base = this.extractedValuesCache[dataType];
+ const part1 = base[which];
+
+ if (which2) {
+ if (!base[which]) { base[which] = {}; }
+ base[which][which2] = values;
+ } else {
+ base[which] = values;
+ }
+
+ return values;
+ }
+
+ getFfPosTable(rtype: string): Promise<any> {
+ const storeKey = 'FFPosTable_' + rtype;
+
+ if (this.ffPosMap[rtype]) {
+ return Promise.resolve(this.ffPosMap[rtype]);
+ }
+
+ this.ffPosMap[rtype] = this.store.getLocalItem(storeKey);
+
+ if (this.ffPosMap[rtype]) {
+ return Promise.resolve(this.ffPosMap[rtype]);
+ }
+
+ return this.net.request(
+ 'open-ils.fielder', 'open-ils.fielder.cmfpm.atomic',
+ {query: {tag: {'!=' : '006'}, rec_type: rtype}}
+
+ ).toPromise().then(table => {
+ this.store.setLocalItem(storeKey, table);
+ return this.ffPosMap[rtype] = table;
+ });
+ }
+
+ getFfValueTable(rtype: string): Promise<any> {
+
+ const storeKey = 'FFValueTable_' + rtype;
+
+ if (this.ffValueMap[rtype]) {
+ return Promise.resolve(this.ffValueMap[rtype]);
+ }
+
+ this.ffValueMap[rtype] = this.store.getLocalItem(storeKey);
+
+ if (this.ffValueMap[rtype]) {
+ return Promise.resolve(this.ffValueMap[rtype]);
+ }
+
+ return this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.biblio.fixed_field_values.by_rec_type', rtype
+
+ ).toPromise().then(table => {
+ this.store.setLocalItem(storeKey, table);
+ return this.ffValueMap[rtype] = table;
+ });
+ }
+
+ loadTagTable(selector?: TagTableSelector): Promise<any> {
+
+ if (selector) {
+ if (!selector.marcFormat) {
+ selector.marcFormat = defaultTagTableSelector.marcFormat;
+ }
+ if (!selector.marcRecordType) {
+ selector.marcRecordType =
+ defaultTagTableSelector.marcRecordType;
+ }
+ } else {
+ selector = defaultTagTableSelector;
+ }
+
+ const cacheKey = 'FFValueTable_' + selector.marcRecordType;
+
+ this.tagMap = this.store.getLocalItem(cacheKey);
+
+ if (this.tagMap) {
+ return Promise.resolve(this.tagMap);
+ }
+
+ return this.fetchTagTable(selector).then(_ => {
+ this.store.setLocalItem(cacheKey, this.tagMap);
+ return this.tagMap;
+ });
+ }
+
+ fetchTagTable(selector?: TagTableSelector): Promise<any> {
+ this.tagMap = [];
+ return this.net.request(
+ 'open-ils.cat',
+ 'open-ils.cat.tag_table.all.retrieve.local',
+ this.auth.token(), selector.marcFormat, selector.marcRecordType
+ ).pipe(tap(tagData => {
+ this.tagMap[tagData.tag] = tagData;
+ })).toPromise();
+ }
+
+ getSubfieldCodes(tag: string): ContextMenuEntry[] {
+ if (!tag || !this.tagMap[tag]) { return null; }
+
+ const cached = this.fromCache('sfcodes', tag);
+
+ const list = this.tagMap[tag].subfields.map(sf => ({
+ value: sf.code,
+ label: `${sf.code}: ${sf.description}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+
+ return this.toCache('sfcodes', tag, null, list);
+ }
+
+ getFieldTags(): ContextMenuEntry[] {
+
+ const cached = this.fromCache('fieldtags');
+ if (cached) { return cached; }
+
+ return Object.keys(this.tagMap)
+ .filter(tag => Boolean(this.tagMap[tag]))
+ .map(tag => ({
+ value: tag,
+ label: `${tag}: ${this.tagMap[tag].name}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+ }
+
+ getSubfieldValues(tag: string, sfCode: string): ContextMenuEntry[] {
+ if (!tag || !this.tagMap[tag]) { return []; }
+
+ const cached = this.fromCache('sfvalues', tag, sfCode)
+ if (cached) { return cached; }
+
+ const list: ContextMenuEntry[] = [];
+
+ this.tagMap[tag].subfields
+ .filter(sf =>
+ sf.code === sfCode && sf.hasOwnProperty('value_list'))
+ .forEach(sf => {
+ sf.value_list.forEach(value => {
+
+ let label = value.description || value.code;
+ let code = value.code || label;
+ if (code !== label) { label = `${code}: ${label}`; }
+
+ list.push({value: code, label: label});
+ })
+ });
+
+ return this.toCache('sfvalues', tag, sfCode, list);
+ }
+
+ getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ContextMenuEntry[] {
+ if (!tag || !this.tagMap[tag]) { return }
+
+ const cached = this.fromCache('indicators', tag, which);
+ if (cached) { return cached; }
+
+ let values = this.tagMap[tag][which];
+ if (!values) { return; }
+
+ values = values.map(value => ({
+ value: value.code,
+ label: `${value.code}: ${value.description}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+
+ return this.toCache('indicators', tag, which, values);
+ }
+
+
+ getFfFieldMeta(fieldCode: string, recordType: string): Promise<IdlObject> {
+ return this.getFfPosTable(recordType).then(table => {
+
+ // Note the AngJS MARC editor stores the full POS table
+ // for all record types in every copy of the table, hence
+ // the seemingly extraneous check in recordType.
+ return table.filter(
+ field =>
+ field.fixed_field === fieldCode
+ && field.rec_type === recordType
+ )[0];
+ });
+ }
+
+
+ // Assumes getFfPosTable and getFfValueTable have already been
+ // invoked for the request record type.
+ getFfValues(fieldCode: string, recordType: string): ContextMenuEntry[] {
+
+ const cached = this.fromCache('ffvalues', recordType, fieldCode);
+ if (cached) { return cached; }
+
+ let values = this.ffValueMap[recordType];
+
+ if (!values || !values[fieldCode]) { return null; }
+
+ // extract the canned set of possible values for our
+ // fixed field. Ignore those whose value exceeds the
+ // specified field length.
+ values = values[fieldCode]
+ .filter(val => val[0].length <= val[2])
+ .map(val => ({value: val[0], label: `${val[0]}: ${val[1]}`}))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+
+ return this.toCache('ffvalues', recordType, fieldCode, values);
+ }
+}
+
+
+
<!-- global print handler component -->
<eg-print></eg-print>
+<!-- context menu DOM insertion point -->
+<eg-context-menu-container></eg-context-menu-container>
background-color: #c9efe4;
color: black;
}
+