// 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;
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) {}
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 = {
fieldId: this.field ? this.field.fieldId : -1,
target: this.fieldType,
- sfOffset: this.subfield ? this.subfield[2] : undefined
+ sfOffset: this.subfield ? this.subfield[2] : undefined,
+ ffCode: this.fixedFieldCode
};
}
this.watchForUndoRedoRequests();
break;
+ case 'ffld':
+ this.applyFFOptions();
+ this.watchForFocusRequests();
+ this.watchForUndoRedoRequests();
+ break;
+
case 'ind1':
case 'ind2':
this.maxLength = 1;
}
}
+ 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[] {
case 'ind2':
return this.tagTable.getIndicatorValues(
this.field.tag, this.fieldType);
+
+ case 'ffld':
+ return this.tagTable.getFfValues(
+ this.fixedFieldCode, this.record.recordType());
}
return null;
case 'tag': return this.field.tag;
case 'sfc': return this.subfield[0];
case 'sfv': return this.subfield[1];
- case 'ind1': // thunk
- case 'ind2': return this.field[this.fieldType];
+ 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) {
case 'tag': this.field.tag = value; break;
case 'sfc': this.subfield[0] = value; break;
case 'sfv': this.subfield[1] = value; break;
- case 'ind1': // thunk
- case 'ind2': this.field[this.fieldType] = 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) {
trackTextChangeForUndo(value: string) {
+ // Human-driven changes invalidate the redo stack.
+ this.context.redoStack = [];
+
const lastUndo = this.context.undoStack[0];
if (lastUndo
inputSize(): number {
if (this.maxLength) {
- return 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;
return;
}
- // None of the remaining key combos are supported by the LDR.
- if (this.fieldType === 'ldr') { 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) {
const STUB_DATA_00X = ' ';
export type MARC_EDITABLE_FIELD_TYPE =
- 'ldr' | 'tag' | 'cfld' | 'ind1' | 'ind2' | 'sfc' | 'sfv';
+ '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 {
}
requestUndo() {
+ console.debug('undo requested with stack size ', this.undoStack.length);
const undo = this.undoStack.shift();
if (undo) {
undo.isRedo = false;
trackStructuralUndo(field: MarcField, isAddition: boolean, subfield?: MarcSubfield) {
+ // Human-driven changes invalidate the redo stack so clear it.
+ this.redoStack = [];
+
const position: FieldFocusRequest = {fieldId: field.fieldId, target: 'tag'};
let prevPos: FieldFocusRequest = null;
<ng-container *ngIf="fieldMeta">
- <div class="form-inline d-flex">
+ <div class="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($event.target.value)"
- [ngModel]="fieldValue"
- [attr.maxlength]="fieldLength" [attr.size]="fieldLength"
- [egContextMenu]="fieldValues"
- (menuItemSelected)="valueChange($event.value)"
- />
+ <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>
-
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, ValueLabelPair} from './tagtable.service';
+import {TagTableService} from './tagtable.service';
/**
* MARC Fixed Field Editing Component
get record(): MarcRecord { return this.context.record; }
- fieldValue: string;
- fieldMeta: any;
- fieldLength: number = null;
- fieldValues: ValueLabelPair[] = null;
+ 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(); }
- this.fieldValues = null;
- return this.tagTable.getFFPosTable(this.record.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.
- this.fieldMeta = table.filter(field =>
- field.fixed_field === this.fieldCode
- && field.rec_type === this.record.recordType())[0];
-
- if (!this.fieldMeta) {
- // Not all record types have all field types.
- return;
- }
-
- this.fieldLength = this.fieldMeta.length || 1;
- this.fieldValue =
- this.context.record.extractFixedField(this.fieldCode);
-
- // Shuffling may occur with our fixed field as a result of
- // external changes.
- this.record.fixedFieldChange.subscribe(_ =>
- this.fieldValue =
- this.context.record.extractFixedField(this.fieldCode)
- );
-
- return this.tagTable.getFFValueTable(this.record.recordType());
-
- }).then(values => {
- if (!values || !values[this.fieldCode]) { return; }
-
- // extract the canned set of possible values for our
- // fixed field. Ignore those whose value exceeds the
- // specified field length.
- this.fieldValues = values[this.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);
- });
- }
-
- valueChange(newVal: string) {
- this.fieldValue = newVal;
- this.context.record.setFixedField(this.fieldCode, this.fieldValue);
+ // 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);
}
}
[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"
</div>
</div>
</div>
+ <div class="col-lg-1">
+ </div>
</div>
-
<div *ngIf="showHelp" class="row m-2">
<div class="col-lg-4">
<ul>
<!-- 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 pr-2">
+ fieldText="LDR" i18n-fieldText moreClasses="p-1">
</eg-marc-editable-content>
<eg-marc-editable-content [context]="context" fieldType="ldr"
- moreClasses="p-1">
+ moreClasses="p-1 pr-2">
</eg-marc-editable-content>
</div>
return Promise.all([
this.tagTable.loadTagTable({marcRecordType: this.context.recordType}),
- this.tagTable.getFFPosTable(this.record.recordType()),
- this.tagTable.getFFValueTable(this.record.recordType())
+ this.tagTable.getFfPosTable(this.record.recordType()),
+ this.tagTable.getFfValueTable(this.record.recordType())
]).then(_ => this.dataLoaded = true);
}
+ undoCount(): number {
+ return this.context.undoStack.length;
+ }
+
+ 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());
}
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';
-
-export interface ValueLabelPair {
- value: string;
- label: string;
-}
+import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
interface TagTableSelector {
marcFormat?: string;
fieldtags: {},
indicators: {},
sfcodes: {},
- sfvalues: {}
+ 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): ValueLabelPair[] {
+ fromCache(dataType: string, which?: string, which2?: string): ContextMenuEntry[] {
const part1 = this.extractedValuesCache[dataType][which];
if (which2) {
if (part1) {
}
toCache(dataType: string, which: string,
- which2: string, values: ValueLabelPair[]): ValueLabelPair[] {
+ which2: string, values: ContextMenuEntry[]): ContextMenuEntry[] {
const base = this.extractedValuesCache[dataType];
const part1 = base[which];
return values;
}
- getFFPosTable(rtype: string): Promise<any> {
+ getFfPosTable(rtype: string): Promise<any> {
const storeKey = 'FFPosTable_' + rtype;
if (this.ffPosMap[rtype]) {
});
}
- getFFValueTable(rtype: string): Promise<any> {
+ getFfValueTable(rtype: string): Promise<any> {
const storeKey = 'FFValueTable_' + rtype;
})).toPromise();
}
- getSubfieldCodes(tag: string): ValueLabelPair[] {
+ getSubfieldCodes(tag: string): ContextMenuEntry[] {
if (!tag || !this.tagMap[tag]) { return null; }
const cached = this.fromCache('sfcodes', tag);
return this.toCache('sfcodes', tag, null, list);
}
- getFieldTags(): ValueLabelPair[] {
+ getFieldTags(): ContextMenuEntry[] {
const cached = this.fromCache('fieldtags');
if (cached) { return cached; }
.sort((a, b) => a.label < b.label ? -1 : 1);
}
- getSubfieldValues(tag: string, sfCode: string): ValueLabelPair[] {
+ 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: ValueLabelPair[] = [];
+ const list: ContextMenuEntry[] = [];
this.tagMap[tag].subfields
.filter(sf =>
return this.toCache('sfvalues', tag, sfCode, list);
}
- getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ValueLabelPair[] {
+ getIndicatorValues(tag: string, which: 'ind1' | 'ind2'): ContextMenuEntry[] {
if (!tag || !this.tagMap[tag]) { return }
const cached = this.fromCache('indicators', tag, which);
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);
+ }
}
+