--- /dev/null
+import {Injectable} from '@angular/core';
+import {CanDeactivate} from '@angular/router';
+import {Observable} from 'rxjs';
+
+/**
+ * https://angular.io/guide/router#candeactivate-handling-unsaved-changes
+ *
+ * routing:
+ * {
+ * path: 'record/:id/:tab',
+ * component: MyComponent,
+ * canDeactivate: [CanDeactivateGuard]
+ * }
+ *
+ * export class MyComponent {
+ * canDeactivate() ... {
+ * ...
+ * }
+ * }
+ */
+
+export interface CanComponentDeactivate {
+ canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
+}
+
+@Injectable({providedIn: 'root'})
+export class CanDeactivateGuard
+ implements CanDeactivate<CanComponentDeactivate> {
+
+ canDeactivate(component: CanComponentDeactivate) {
+ return component.canDeactivate ? component.canDeactivate() : true;
+ }
+}
</eg-title>
</ng-container>
+<eg-confirm-dialog #pendingChangesDialog
+ i18n-dialogTitle dialogTitle="Unsaved Changes Confirmation"
+ i18n-dialogBoby dialogBody="Unsaved changes will be lost. Continue navigation?">
+</eg-confirm-dialog>
+
<div id="staff-catalog-record-container">
<div id='staff-catalog-bib-summary-container' class='mb-1'>
<eg-bib-summary [bibSummary]="summary">
(click)="setDefaultTab()" i18n>Set Default View</button>
</div>
</div>
- <ngb-tabset #recordTabs [activeId]="recordTab" (tabChange)="onTabChange($event)">
+ <ngb-tabset #recordTabs [activeId]="recordTab"
+ (tabChange)="beforeTabChange($event)">
<ngb-tab title="Item Table" i18n-title id="catalog">
<ng-template ngbTabContent>
<eg-catalog-copies [recordId]="recordId"></eg-catalog-copies>
<ngb-tab title="MARC Edit" i18n-title id="marc_edit">
<ng-template ngbTabContent>
<div class="mt-3">
- <eg-marc-editor (recordSaved)="handleMarcRecordSaved()"
+ <eg-marc-editor #marcEditor (recordSaved)="handleMarcRecordSaved()"
[recordId]="recordId"></eg-marc-editor>
</div>
</ng-template>
-import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Component, OnInit, Input, ViewChild, HostListener} from '@angular/core';
import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
import {PcrudService} from '@eg/core/pcrud.service';
import {StaffCatalogService} from '../catalog.service';
import {BibSummaryComponent} from '@eg/staff/share/bib-summary/bib-summary.component';
import {StoreService} from '@eg/core/store.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {MarcEditorComponent} from '@eg/staff/share/marc-edit/editor.component';
@Component({
selector: 'eg-catalog-record',
summary: BibRecordSummary;
searchContext: CatalogSearchContext;
@ViewChild('recordTabs', { static: true }) recordTabs: NgbTabset;
+ @ViewChild('marcEditor', {static: false}) marcEditor: MarcEditorComponent;
defaultTab: string; // eg.cat.default_record_tab
+ @ViewChild('pendingChangesDialog', {static: false})
+ pendingChangesDialog: ConfirmDialogComponent;
+
constructor(
private router: Router,
private route: ActivatedRoute,
// Changing a tab in the UI means changing the route.
// Changing the route ultimately results in changing the tab.
- onTabChange(evt: NgbTabChangeEvent) {
- this.recordTab = evt.nextId;
+ beforeTabChange(evt: NgbTabChangeEvent) {
// prevent tab changing until after route navigation
evt.preventDefault();
- this.routeToTab();
+ // Protect against tab changes with dirty data.
+ this.canDeactivate().then(ok => {
+ if (ok) {
+ this.recordTab = evt.nextId;
+ this.routeToTab();
+ }
+ });
+ }
+
+ /*
+ * Handle 3 types of navigation which can cause loss of data.
+ * 1. Record detail tab navigation (see also beforeTabChange())
+ * 2. Intra-Angular route navigation away from the record detail page
+ * 3. Browser page unload/reload
+ *
+ * For the #1, and #2, display a eg confirmation dialog.
+ * For #3 use the stock browser onbeforeunload dialog.
+ *
+ * Note in this case a tab change is a route change, but it's one
+ * which does not cause RecordComponent to unload, so it has to be
+ * manually tracked in beforeTabChange().
+ */
+ @HostListener('window:beforeunload', ['$event'])
+ canDeactivate($event?: Event): Promise<boolean> {
+
+ if (this.marcEditor && this.marcEditor.changesPending()) {
+
+ // Each warning dialog clears the current "changes are pending"
+ // flag so the user is not presented with the dialog again
+ // unless new changes are made.
+ this.marcEditor.clearPendingChanges();
+
+ if ($event) { // window.onbeforeunload
+ $event.preventDefault();
+ $event.returnValue = true;
+
+ } else { // tab OR route change.
+ return this.pendingChangesDialog.open().toPromise();
+ }
+
+ } else {
+ return Promise.resolve(true);
+ }
}
routeToTab() {
import {HoldComponent} from './hold/hold.component';
import {BrowseComponent} from './browse.component';
import {CnBrowseComponent} from './cnbrowse.component';
+import {CanDeactivateGuard} from '@eg/share/util/can-deactivate.guard';
const routes: Routes = [{
path: '',
component: HoldComponent
}, {
path: 'record/:id/:tab',
- component: RecordComponent
+ component: RecordComponent,
+ canDeactivate: [CanDeactivateGuard]
}]}, {
// Browse is a top-level UI
path: 'browse',
undo.position = this.context.lastFocused;
undo.textContent = this.undoBackToText;
- this.context.undoStack.unshift(undo);
+ this.context.addToUndoStack(undo);
}
// Apply the undo or redo action and track its opposite
this.context.deleteField(this.field);
evt.preventDefault();
- } else if (evt.shiftKey && this.subfield) {
- // shift+delete == delete subfield
+ } else if (evt.shiftKey) {
- this.context.deleteSubfield(this.field, this.subfield);
+ if (this.subfield) {
+ // shift+delete == delete subfield
+
+ this.context.deleteSubfield(this.field, this.subfield);
+ }
+ // prevent any shift-delete from bubbling up becuase
+ // unexpected stuff will be deleted.
evt.preventDefault();
}
undoStack: UndoRedoAction[] = [];
redoStack: UndoRedoAction[] = [];
+ // True if any changes have been made.
+ // For the 'rich' editor, this is any un-do-able actions.
+ // For the text edtior it's any text change.
+ changesPending: boolean;
+
private _record: MarcRecord;
set record(r: MarcRecord) {
if (r !== this._record) {
}
}
+ addToUndoStack(action: UndoRedoAction) {
+ this.changesPending = true;
+ this.undoStack.unshift(action);
+ }
+
handleStructuralUndoRedo(action: StructUndoRedoAction) {
if (action.wasAddition) {
// time of action will be different than the added field.
action.prevFocus = this.lastFocused;
- this.undoStack.unshift(action);
+ this.addToUndoStack(action);
}
deleteField(field: MarcField) {
);
}
+ changesPending(): boolean {
+ return this.context.changesPending;
+ }
+
+ clearPendingChanges() {
+ this.context.changesPending = false;
+ }
+
// Remember the last used tab as the preferred tab.
tabChange(evt: NgbTabChangeEvent) {
saveRecord(): Promise<any> {
const xml = this.record.toXml();
+ // Save actions clears any pending changes.
+ this.context.changesPending = false;
+ this.context.resetUndos();
+
let sourceName: string = null;
let sourceId: number = null;
<div *ngIf="record">
<textarea class="form-control flat-editor-content"
+ (change)="textChanged()"
(blur)="record.absorbBreakerChanges()"
[(ngModel)]="record.breakerText" rows="{{rowCount()}}" spellcheck="false">
</textarea>
}
return 40;
}
+
+ textChanged() {
+ this.context.changesPending = true;
+ }
}