--- /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-template #menuTemplate>
+ <div *ngFor="let entry of menuEntries" class="menu-entry {{entryClasses}}">
+ <a (click)="entryClicked(entry)">
+ <span>{{entry.label}}</span>
+ </a>
+ </div>
+</ng-template>
+
+<!--
+<div class="form-inline d-flex">
+ <div class="flex-4">
+ <span id='label-{{randId}}' class="text-left font-weight-bold">
+ {{fieldLabel}}
+ </span>
+ </div>
+ <input
+ [attr.aria-labelledby]="'label-' + randId"
+ class="form-control rounded-0 flex-5" type="text"
+ (change)="valueChange()"
+ [(ngModel)]="fieldValue"
+ [attr.maxlength]="fieldLength" [attr.size]="fieldLength"
+ [ngbPopover]="menuContent"
+ #p="ngbPopover" triggers="manual"
+ (contextmenu)="contextMenu($event, p)"
+ />
+</div>
+-->
+
--- /dev/null
+import {Component, Input, Output, OnInit, EventEmitter, Directive,
+ ViewChild, TemplateRef} from '@angular/core';
+import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * Right-click context menu component.
+ *
+ * No state is maintained (i.e. no current value), events are
+ * simply emitted as entries are chosen.
+ *
+ * <eg-context-menu-entries #tRef [menuEntries]="ContextMenuEntry[]"
+ * (entrySelected)="$event === ContextMenuEntry">
+ * <input ... [egContextMenu]="tRef" ... />
+ */
+
+export interface ContextMenuEntry {
+ value: string;
+ label: string;
+}
+
+@Component({
+ selector: 'eg-context-menu-entries',
+ templateUrl: './context-menu.component.html',
+ styleUrls: ['context-menu.component.css']
+})
+
+export class ContextMenuComponent implements OnInit {
+
+ @Input() menuEntries: ContextMenuEntry[] = [];
+
+ // Additional CSS classes (space separated) to apply to the entry links
+ @Input() entryClasses = '';
+
+ @Output() entrySelected: EventEmitter<ContextMenuEntry>;
+
+ @ViewChild('menuTemplate', {static: false}) public menuTemplate: TemplateRef<any>;
+
+ constructor() {
+ this.entrySelected = new EventEmitter<ContextMenuEntry>();
+ }
+
+ ngOnInit() {}
+
+ entryClicked(entry: ContextMenuEntry) {
+ this.entrySelected.emit(entry);
+ }
+}
+
+
+@Directive({
+ selector: '[egContextMenu]',
+ exportAs: 'egContextMenu'
+})
+export class ContextMenuDirective extends NgbPopover {
+
+ @Input() set egContextMenu(menuComp: ContextMenuComponent) {
+ this.ngbPopover = menuComp.menuTemplate;
+ }
+
+ // Only one active menu is allowed at a time.
+ static activeMenu: ContextMenuDirective;
+
+ open() {
+ super.open();
+ if (ContextMenuDirective.activeMenu) {
+ ContextMenuDirective.activeMenu.close();
+ }
+ ContextMenuDirective.activeMenu = this;
+ }
+}
+
+
// NgbPopovers don't always close when we want them to,
// specifcially when context-clicking to open other popovers.
closePopovers() {
+ // TODO
this.popOvers.forEach(p => p.close());
}
import {Component, Input, Output, OnInit, EventEmitter} from '@angular/core';
import {MarcRecord} from './marcrecord';
import {MarcEditContext} from './editor-context';
-import {TagTableService} from './tagtable.service';
+import {TagTableService, ValueLabelPair} from './tagtable.service';
import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
/**
* MARC Fixed Field Editing Component
*/
-interface FixedFieldValue {
- value: string;
- label: string;
-}
-
@Component({
selector: 'eg-fixed-field',
templateUrl: './fixed-field.component.html',
fieldValue: string;
fieldMeta: any;
fieldLength: number = null;
- fieldValues: FixedFieldValue[] = null;
+ fieldValues: ValueLabelPair[] = null;
popOver: NgbPopover;
randId = Math.floor(Math.random() * 10000000);
import {TagTableService} from './tagtable.service';
import {EditableContentComponent} from './editable-content.component';
+// TODO: consider moving to share
+import {ContextMenuDirective, ContextMenuComponent} from './context-menu.component';
+
@NgModule({
declarations: [
MarcEditorComponent,
MarcFlatEditorComponent,
FixedFieldsEditorComponent,
FixedFieldComponent,
- EditableContentComponent
+ EditableContentComponent,
+ ContextMenuDirective,
+ ContextMenuComponent
],
imports: [
StaffCommonModule
if (!this.record) { return Promise.resolve(); }
return Promise.all([
+ this.tagTable.loadTagTable({marcRecordType: this.record.recordType()}),
this.tagTable.getFFPosTable(this.record.recordType()),
this.tagTable.getFFValueTable(this.record.recordType())
]).then(_ => this.dataLoaded = true);
import {Injectable, EventEmitter} from '@angular/core';
-import {map} from 'rxjs/operators';
+import {map, tap} from 'rxjs/operators';
import {StoreService} from '@eg/core/store.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';
+export interface ValueLabelPair {
+ value: string;
+ label: string;
+}
+
+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} = {};
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;
+ }
+
+ // TODO load from local store cache
+
+ return this.fetchTagTable(selector);
+ }
+
+ fetchTagTable(selector?: TagTableSelector): Promise<any> {
+ 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): ValueLabelPair[] {
+ if (!tag || !this.tagMap[tag]) { return null; }
+
+ return this.tagMap[tag].subfields
+ .map(sf => ({
+ value: sf.code,
+ label: `${sf.code}: ${sf.description}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+ }
+
+ getFieldTags(): ValueLabelPair[] {
+ return Object.keys(this.tagMap)
+ .map(tag => ({
+ value: tag,
+ label: `${tag}: ${this.tagMap[tag].name}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+ }
+
+ getSubfieldValues(tag: string, sfCode: string): ValueLabelPair[] {
+ if (!tag || !this.tagMap[tag]) { return []; }
+
+ const list: ValueLabelPair[] = [];
+
+ this.tagMap[tag].subfields
+ .filter(sf =>
+ sf.code === sfCode && sf.hasOwnProperty('value_list'))
+ .forEach(sf => {
+ sf.value_list.forEach(value => {
+
+ const label = (value.code == value.description) ?
+ value.code : `${value.code}: ${value.description}`;
+
+ list.push({value: value.code, label: label});
+ })
+ });
+
+ return list.sort((a, b) => a.label < b.label ? -1 : 1);
+ }
+
+ getIndicatorValues(tag: string, pos: number): ValueLabelPair[] {
+ if (!tag || !this.tagMap[tag]) { return }
+
+ const value = this.tagMap[tag][`ind${pos}`];
+ if (!value) { return; }
+
+ return value.map(tag => ({
+ value: value.code,
+ label: `${value.code}: ${value.description}`
+ }))
+ .sort((a, b) => a.label < b.label ? -1 : 1);
+ }
}