@teipublisher/pb-components 2.26.1-next.3 → 3.0.0-next-4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docker-cypress.yml +53 -0
- package/.github/workflows/node.js.yml +70 -21
- package/.releaserc.json +7 -2
- package/CHANGELOG.md +363 -11
- package/Dockerfile +78 -70
- package/README.md +112 -4
- package/css/components.css +5 -5
- package/css/gridjs/mermaid.min.css +1 -1
- package/css/leaflet/Control.Geocoder.css +1 -126
- package/css/leaflet/images/layers.png +0 -0
- package/css/tify/tify.css +6 -5
- package/css/tom-select/tom-select.bootstrap4.min.css +1 -1
- package/css/tom-select/tom-select.bootstrap5.min.css +1 -1
- package/css/tom-select/tom-select.default.min.css +1 -1
- package/css/tom-select/tom-select.default.min.css.map +1 -0
- package/css/tom-select/tom-select.min.css +1 -1
- package/cypress.config.js +84 -0
- package/dist/api.html +1 -1
- package/dist/css/design-system.css +607 -0
- package/dist/demo/bundle-test.html +4 -3
- package/dist/demo/components.css +46 -1
- package/dist/demo/design-system.html +710 -0
- package/dist/demo/dts-client.html +2 -2
- package/dist/demo/pb-autocomplete.html +23 -11
- package/dist/demo/pb-autocomplete2.html +66 -55
- package/dist/demo/pb-autocomplete3.html +17 -8
- package/dist/demo/pb-blacklab-highlight.html +28 -11
- package/dist/demo/pb-blacklab-results.html +3 -2
- package/dist/demo/pb-browse-docs.html +24 -24
- package/dist/demo/pb-browse-docs2.html +3 -3
- package/dist/demo/pb-clipboard.html +32 -28
- package/dist/demo/pb-code-editor.html +6 -6
- package/dist/demo/pb-code-highlight.html +63 -63
- package/dist/demo/pb-codepen.html +1 -1
- package/dist/demo/pb-collapse.html +1 -1
- package/dist/demo/pb-collapse2.html +2 -2
- package/dist/demo/pb-combo-box.html +135 -130
- package/dist/demo/pb-custom-form.html +64 -55
- package/dist/demo/pb-dialog.html +12 -6
- package/dist/demo/pb-document.html +1 -1
- package/dist/demo/pb-download.html +68 -59
- package/dist/demo/pb-drawer.html +67 -46
- package/dist/demo/pb-drawer2.html +65 -58
- package/dist/demo/pb-edit-app.html +2 -2
- package/dist/demo/pb-edit-xml.html +1 -1
- package/dist/demo/pb-facsimile-2.html +26 -11
- package/dist/demo/pb-facsimile-3.html +25 -10
- package/dist/demo/pb-facsimile-dedup-test-2.html +48 -0
- package/dist/demo/pb-facsimile-dedup-test.html +48 -0
- package/dist/demo/pb-facsimile.html +4 -4
- package/dist/demo/pb-formula.html +1 -1
- package/dist/demo/pb-grid.html +22 -8
- package/dist/demo/pb-highlight.html +2 -2
- package/dist/demo/pb-i18n-simple.html +1 -0
- package/dist/demo/pb-i18n.html +15 -5
- package/dist/demo/pb-image-strip-standalone.html +2 -2
- package/dist/demo/pb-image-strip-view.html +2 -2
- package/dist/demo/pb-leaflet-map.html +3 -3
- package/dist/demo/pb-leaflet-map2.html +2 -2
- package/dist/demo/pb-leaflet-map3.html +3 -3
- package/dist/demo/pb-link.html +1 -1
- package/dist/demo/pb-load.html +2 -6
- package/dist/demo/pb-login.html +1 -3
- package/dist/demo/pb-manage-odds.html +9 -4
- package/dist/demo/pb-markdown.html +1 -1
- package/dist/demo/pb-media-query.html +2 -2
- package/dist/demo/pb-mei.html +2 -2
- package/dist/demo/pb-mei2.html +2 -2
- package/dist/demo/pb-message.html +2 -3
- package/dist/demo/pb-odd-editor.html +54 -52
- package/dist/demo/pb-page-header.html +27 -0
- package/dist/demo/pb-popover.html +1 -1
- package/dist/demo/pb-print-preview.html +2 -2
- package/dist/demo/pb-progress.html +4 -4
- package/dist/demo/pb-repeat.html +32 -36
- package/dist/demo/pb-search.html +16 -5
- package/dist/demo/pb-search2.html +4 -4
- package/dist/demo/pb-search3.html +3 -3
- package/dist/demo/pb-search4.html +3 -3
- package/dist/demo/pb-select-feature.html +4 -4
- package/dist/demo/pb-select-feature2.html +4 -4
- package/dist/demo/pb-select-feature3.html +2 -2
- package/dist/demo/pb-select-i18n.html +58 -53
- package/dist/demo/pb-select-odd.html +1 -1
- package/dist/demo/pb-select.html +190 -75
- package/dist/demo/pb-select2.html +91 -37
- package/dist/demo/pb-select3.html +109 -41
- package/dist/demo/pb-svg.html +1 -1
- package/dist/demo/pb-table-grid.html +26 -15
- package/dist/demo/pb-tabs.html +15 -7
- package/dist/demo/pb-tify.html +7 -7
- package/dist/demo/pb-timeline.html +1 -1
- package/dist/demo/pb-timeline2.html +1 -1
- package/dist/demo/pb-toggle-feature.html +26 -23
- package/dist/demo/pb-toggle-feature2.html +4 -4
- package/dist/demo/pb-toggle-feature3.html +2 -2
- package/dist/demo/pb-toggle-feature4.html +56 -54
- package/dist/demo/pb-version.html +2 -2
- package/dist/demo/pb-view.html +78 -40
- package/dist/demo/pb-view2.html +69 -46
- package/dist/demo/pb-view3.html +53 -48
- package/dist/demo/pb-view4.html +70 -49
- package/dist/demo/pb-zoom.html +2 -2
- package/dist/{es-global-bridge-d8ce175d.js → es-global-bridge-D8ZcUcx_.js} +0 -4
- package/dist/focus-mixin-VCsFap6b.js +768 -0
- package/dist/images/icons.svg +217 -0
- package/dist/jinn-codemirror-DETLdm08.js +1 -0
- package/dist/lib/openseadragon.min.js +80 -0
- package/dist/lib/openseadragon.min.js.map +1 -0
- package/dist/pb-code-editor.js +25 -20
- package/dist/pb-component-docs.js +414 -3225
- package/dist/pb-components-bundle.js +3046 -4402
- package/dist/pb-dialog-tklYGWfc.js +121 -0
- package/dist/pb-edit-app.js +208 -107
- package/dist/pb-elements.json +716 -249
- package/dist/pb-facsimile.js +46 -0
- package/dist/pb-i18n-C0NDma4h.js +1 -0
- package/dist/pb-leaflet-map.js +23 -23
- package/dist/pb-mei.js +152 -134
- package/dist/pb-mixin-DHoWQheB.js +1 -0
- package/dist/pb-odd-editor.js +1671 -1231
- package/dist/pb-tify.js +1 -27
- package/dist/unsafe-html-D5VGo9Oq.js +1 -0
- package/dist/urls-BEONu_g4.js +1 -0
- package/eslint.config.mjs +92 -0
- package/gh-pages.js +5 -3
- package/i18n/common/en.json +6 -0
- package/i18n/common/pl.json +2 -2
- package/images/icons.svg +217 -0
- package/index.html +0 -5
- package/lib/leaflet-src.js.map +1 -0
- package/lib/leaflet.markercluster-src.js.map +1 -0
- package/lib/openseadragon.min.js +6 -6
- package/package.json +56 -81
- package/pb-elements.json +716 -249
- package/rollup.config.mjs +312 -0
- package/src/assets/components.css +5 -5
- package/src/assets/design-system.css +607 -0
- package/src/authority/airtable.js +20 -21
- package/src/authority/anton.js +129 -129
- package/src/authority/custom.js +70 -27
- package/src/authority/geonames.js +38 -32
- package/src/authority/gnd.js +50 -42
- package/src/authority/kbga.js +136 -134
- package/src/authority/metagrid.js +44 -46
- package/src/authority/reconciliation.js +66 -68
- package/src/authority/registry.js +4 -4
- package/src/docs/demo-utils.js +91 -0
- package/src/docs/pb-component-docs.js +287 -147
- package/src/docs/pb-component-view.js +380 -273
- package/src/docs/pb-components-list.js +115 -51
- package/src/docs/pb-demo-snippet.js +199 -174
- package/src/dts-client.js +306 -303
- package/src/dts-select-endpoint.js +125 -85
- package/src/parse-date-service.js +184 -135
- package/src/pb-ajax.js +175 -173
- package/src/pb-authority-lookup.js +198 -158
- package/src/pb-autocomplete.js +731 -313
- package/src/pb-blacklab-highlight.js +266 -260
- package/src/pb-blacklab-results.js +230 -225
- package/src/pb-browse-docs.js +601 -484
- package/src/pb-browse.js +68 -65
- package/src/pb-clipboard.js +97 -76
- package/src/pb-code-editor.js +111 -103
- package/src/pb-code-highlight.js +234 -204
- package/src/pb-codepen.js +81 -73
- package/src/pb-collapse.js +265 -152
- package/src/pb-combo-box.js +191 -191
- package/src/pb-components-bundle.js +1 -7
- package/src/pb-components.js +2 -6
- package/src/pb-custom-form.js +230 -141
- package/src/pb-dialog.js +99 -63
- package/src/pb-document.js +118 -91
- package/src/pb-download.js +214 -198
- package/src/pb-drawer.js +146 -149
- package/src/pb-edit-app.js +471 -240
- package/src/pb-edit-xml.js +101 -98
- package/src/pb-events.js +126 -107
- package/src/pb-facs-link.js +130 -101
- package/src/pb-facsimile.js +494 -410
- package/src/pb-fetch.js +389 -0
- package/src/pb-formula.js +152 -154
- package/src/pb-geolocation.js +130 -132
- package/src/pb-grid-action.js +59 -56
- package/src/pb-grid.js +388 -228
- package/src/pb-highlight.js +142 -142
- package/src/pb-hotkeys.js +40 -42
- package/src/pb-i18n.js +115 -127
- package/src/pb-icon-button.js +108 -0
- package/src/pb-icon.js +283 -0
- package/src/pb-image-strip.js +85 -79
- package/src/pb-lang.js +142 -57
- package/src/pb-leaflet-map.js +551 -483
- package/src/pb-link.js +132 -126
- package/src/pb-load.js +495 -428
- package/src/pb-login.js +303 -248
- package/src/pb-manage-odds.js +384 -338
- package/src/pb-map-icon.js +90 -90
- package/src/pb-map-layer.js +86 -86
- package/src/pb-markdown.js +107 -110
- package/src/pb-media-query.js +75 -73
- package/src/pb-mei.js +523 -303
- package/src/pb-message.js +144 -98
- package/src/pb-mixin.js +268 -265
- package/src/pb-navigation.js +83 -96
- package/src/pb-observable.js +39 -39
- package/src/pb-odd-editor.js +1209 -948
- package/src/pb-odd-elementspec-editor.js +375 -310
- package/src/pb-odd-model-editor.js +1189 -941
- package/src/pb-odd-parameter-editor.js +269 -170
- package/src/pb-odd-rendition-editor.js +184 -131
- package/src/pb-page.js +451 -422
- package/src/pb-paginate.js +260 -178
- package/src/pb-panel.js +217 -183
- package/src/pb-popover-themes.js +16 -9
- package/src/pb-popover.js +297 -288
- package/src/pb-print-preview.js +128 -128
- package/src/pb-progress.js +52 -52
- package/src/pb-repeat.js +141 -108
- package/src/pb-restricted.js +85 -78
- package/src/pb-search.js +258 -230
- package/src/pb-select-feature.js +210 -126
- package/src/pb-select-odd.js +184 -118
- package/src/pb-select-template.js +113 -78
- package/src/pb-select.js +330 -229
- package/src/pb-split-list.js +181 -176
- package/src/pb-svg.js +81 -80
- package/src/pb-table-column.js +55 -55
- package/src/pb-table-grid.js +334 -205
- package/src/pb-tabs.js +238 -61
- package/src/pb-tify.js +3331 -126
- package/src/pb-timeline.js +394 -255
- package/src/pb-toggle-feature.js +196 -188
- package/src/pb-upload.js +201 -176
- package/src/pb-version.js +22 -34
- package/src/pb-view-annotate.js +138 -102
- package/src/pb-view.js +1722 -1272
- package/src/pb-zoom.js +144 -46
- package/src/search-result-service.js +256 -223
- package/src/seed-element.js +14 -22
- package/src/settings.js +4 -4
- package/src/theming.js +98 -91
- package/src/urls.js +403 -289
- package/src/utils.js +53 -51
- package/vite.config.js +86 -0
- package/.github/workflows/main.yml +0 -24
- package/.github/workflows/release.js.yml +0 -34
- package/css/pb-styles.css +0 -51
- package/dist/iron-form-3b8dcaa7.js +0 -210
- package/dist/jinn-codemirror-da0e2d1f.js +0 -1
- package/dist/paper-checkbox-515a5284.js +0 -1597
- package/dist/paper-icon-button-b1d31571.js +0 -398
- package/dist/paper-listbox-a3b7175c.js +0 -1265
- package/dist/pb-i18n-0611135a.js +0 -1
- package/dist/pb-mixin-b1caa22e.js +0 -158
- package/dist/polymer-hack.js +0 -1
- package/dist/vaadin-element-mixin-fe4a4883.js +0 -527
- package/lib/Control.Geocoder.min.js +0 -2
- package/lib/Control.Geocoder.min.js.map +0 -1
- package/src/assets/pb-styles.css +0 -51
- package/src/pb-light-dom.js +0 -41
- package/src/polymer-hack.js +0 -6
package/src/pb-tify.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { LitElement } from 'lit
|
|
2
|
-
import
|
|
3
|
-
import { pbMixin, waitOnce } from './pb-mixin.js';
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
import 'tify';
|
|
3
|
+
import { pbMixin, waitOnce, defaultChannel, getEmittedChannels } from './pb-mixin.js';
|
|
4
4
|
import { resolveURL } from './utils.js';
|
|
5
|
+
import { registry } from './urls.js';
|
|
5
6
|
|
|
6
7
|
function _injectStylesheet(path) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
const style = document.querySelector(`link#pb-tify`);
|
|
9
|
+
if (!style) {
|
|
10
|
+
const elem = document.createElement('link');
|
|
11
|
+
elem.type = 'text/css';
|
|
12
|
+
elem.rel = 'stylesheet';
|
|
13
|
+
elem.id = `pb-tify`;
|
|
14
|
+
elem.href = `${resolveURL(path)}/tify.css`;
|
|
15
|
+
document.head.appendChild(elem);
|
|
16
|
+
}
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -26,170 +27,3374 @@ function _injectStylesheet(path) {
|
|
|
26
27
|
* `order` property in the event (see `pb-facs-link`). Page counts start at 1.
|
|
27
28
|
*/
|
|
28
29
|
export class PbTify extends pbMixin(LitElement) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
30
|
+
static get properties() {
|
|
31
|
+
return {
|
|
32
|
+
/**
|
|
33
|
+
* URL pointing to a IIIF presentation manifest. Relative paths
|
|
34
|
+
* are interpreted relative to the API endpoint.
|
|
35
|
+
*/
|
|
36
|
+
manifest: {
|
|
37
|
+
type: String,
|
|
38
|
+
},
|
|
39
|
+
/**
|
|
40
|
+
* If true, pb-tify will respond to pb-navigate events to synchronize
|
|
41
|
+
* with navigation buttons. Defaults to true.
|
|
42
|
+
*/
|
|
43
|
+
enableNavigation: {
|
|
44
|
+
type: Boolean,
|
|
45
|
+
attribute: 'enable-navigation',
|
|
46
|
+
reflect: true,
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* The initial view to display. Can be 'toc' (table of contents),
|
|
50
|
+
* 'thumbnails', 'text', 'info', 'help', 'export', or null for media only.
|
|
51
|
+
* Defaults to null (media view only).
|
|
52
|
+
*/
|
|
53
|
+
initialView: {
|
|
54
|
+
type: String,
|
|
55
|
+
attribute: 'initial-view',
|
|
56
|
+
reflect: true,
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* If set, do not update browser history when navigating.
|
|
60
|
+
* Defaults to false (history is enabled).
|
|
61
|
+
*/
|
|
62
|
+
disableHistory: {
|
|
63
|
+
type: Boolean,
|
|
64
|
+
attribute: 'disable-history',
|
|
65
|
+
reflect: true,
|
|
66
|
+
},
|
|
67
|
+
...super.properties,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
super();
|
|
73
|
+
this.cssPath = '../css/tify';
|
|
74
|
+
this._initialPages = null;
|
|
75
|
+
this._currentPage = null;
|
|
76
|
+
this.enableNavigation = true;
|
|
77
|
+
this.disableHistory = false;
|
|
78
|
+
this._pendingNavigation = null; // Queue for navigation when viewer isn't ready
|
|
79
|
+
this._pendingPage = null; // Queue for page navigation when canvases aren't ready
|
|
80
|
+
this._canvasWaitStart = null; // Track when we started waiting for canvases
|
|
81
|
+
this._manifestUrl = null; // Store the manifest URL for fallback fetching
|
|
82
|
+
this._cachedManifest = null; // Cache the fetched manifest
|
|
83
|
+
this._lastSwitchPageTime = 0; // Track when we last called _switchPage to prevent rapid-fire events
|
|
84
|
+
this._lastSwitchPageCanvasId = null; // Track the last canvas we switched to
|
|
85
|
+
// Simplified state management - single object replaces multiple flags
|
|
86
|
+
this._navigationState = null; // { source, targetPage, targetId, timestamp, isActive }
|
|
87
|
+
|
|
88
|
+
// Initial load flag - still needed for initial page load sequence
|
|
89
|
+
this._initialLoadComplete = false;
|
|
90
|
+
this._lastSetPage = null; // Track last page set to prevent duplicates
|
|
91
|
+
this._lastSetPageTime = 0; // Track when we last set a page
|
|
92
|
+
this._pageToRootMap = null; // Cache: page number (e.g., "011") -> root (e.g., "3.5.6.1")
|
|
93
|
+
this._fetchingPageToRootMap = false; // Flag to prevent concurrent fetches
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Ensure a mount container exists and is attached.
|
|
97
|
+
* Idempotent: safe to call multiple times.
|
|
98
|
+
*/
|
|
99
|
+
_ensureContainer() {
|
|
100
|
+
if (!this._container) {
|
|
101
|
+
this._container = document.createElement('div');
|
|
102
|
+
this._container.style.height = '100%';
|
|
103
|
+
this._container.style.width = '100%';
|
|
104
|
+
this.appendChild(this._container);
|
|
40
105
|
}
|
|
106
|
+
}
|
|
107
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
108
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
41
109
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this._initialPages = null;
|
|
46
|
-
this._currentPage = null;
|
|
110
|
+
if (name === 'manifest' && newVal) {
|
|
111
|
+
this.manifest = newVal;
|
|
112
|
+
this._initViewer();
|
|
47
113
|
}
|
|
114
|
+
}
|
|
48
115
|
|
|
49
|
-
|
|
50
|
-
|
|
116
|
+
async connectedCallback() {
|
|
117
|
+
super.connectedCallback();
|
|
118
|
+
|
|
119
|
+
_injectStylesheet(this.cssPath);
|
|
120
|
+
|
|
121
|
+
// Ensure container exists even if _initViewer ran early
|
|
122
|
+
this._ensureContainer();
|
|
123
|
+
|
|
124
|
+
// Read initial URL state early (like pb-view does)
|
|
125
|
+
// This ensures we know what page to load before Tify initializes
|
|
126
|
+
if (!this.disableHistory) {
|
|
127
|
+
const state = registry.getState(this);
|
|
128
|
+
// Store whether URL has a page (id or root parameter)
|
|
129
|
+
// We'll resolve the actual page number when Tify is ready
|
|
130
|
+
this._hasInitialUrlPage = !!(state.id || state.root);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.subscribeTo('pb-show-annotation', ev => {
|
|
134
|
+
if (ev.detail) {
|
|
135
|
+
const order = ev.detail.order ? Number(ev.detail.order) : Number.POSITIVE_INFINITY;
|
|
136
|
+
const pageOrder = order === Number.POSITIVE_INFINITY ? 1 : order;
|
|
51
137
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
138
|
+
// CRITICAL FIX: Filter out auto-triggered pb-show-annotation events from pb-facs-link elements
|
|
139
|
+
// that don't match the current registry state. This prevents newly loaded content from
|
|
140
|
+
// triggering unwanted navigation. BUT: Always allow navigation if Tify is on a different page
|
|
141
|
+
// (user might be navigating) or if the event matches the registry state.
|
|
142
|
+
if (!this.disableHistory && this._initialLoadComplete && this._tify && this._tify.viewer) {
|
|
143
|
+
const registryState = registry.getState(this);
|
|
144
|
+
|
|
145
|
+
// Check what page Tify is currently showing
|
|
146
|
+
let currentTifyPage = null;
|
|
147
|
+
try {
|
|
148
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
149
|
+
const page = this._tify.viewer.currentPage();
|
|
150
|
+
if (typeof page === 'number') {
|
|
151
|
+
currentTifyPage = page + 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Ignore
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If Tify is already on the target page, ignore the event (no need to navigate)
|
|
159
|
+
if (currentTifyPage === pageOrder) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Try to get the page ID for this order number to compare with registry
|
|
164
|
+
const root = this._getRootFromApp();
|
|
165
|
+
if (root && registryState.id) {
|
|
166
|
+
const canvases = this._getCanvases(root);
|
|
167
|
+
if (canvases.length > 0 && pageOrder >= 1 && pageOrder <= canvases.length) {
|
|
168
|
+
const canvas = canvases[pageOrder - 1];
|
|
169
|
+
|
|
170
|
+
// Extract page ID from canvas (label or rendering URL)
|
|
171
|
+
let pageId = null;
|
|
172
|
+
|
|
173
|
+
// Try to get ID from canvas label (e.g., "003" -> "A-N-38_003.jpg")
|
|
174
|
+
if (canvas.label) {
|
|
175
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
176
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
177
|
+
if (labelStr) {
|
|
178
|
+
const pathParts = window.location.pathname.split('/');
|
|
179
|
+
let docId = null;
|
|
180
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
181
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
182
|
+
docId = pathParts[i];
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (docId) {
|
|
187
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
188
|
+
pageId = `${docId}_${pageNum}.jpg`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Try to get ID from rendering URL
|
|
194
|
+
if (!pageId && canvas.rendering && canvas.rendering.length > 0) {
|
|
195
|
+
const renderingId = canvas.rendering[0]['@id'] || canvas.rendering[0].id;
|
|
196
|
+
if (renderingId) {
|
|
197
|
+
try {
|
|
198
|
+
const url = new URL(renderingId, window.location.origin);
|
|
199
|
+
pageId = url.searchParams.get('id');
|
|
200
|
+
} catch (e) {
|
|
201
|
+
// Ignore
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Only block if: event page ID doesn't match registry AND Tify is on registry page
|
|
207
|
+
// This means it's a stale auto-trigger from content loading
|
|
208
|
+
// If Tify is on a different page, allow it (user might be navigating)
|
|
209
|
+
if (pageId && pageId !== registryState.id) {
|
|
210
|
+
// Find what page order corresponds to the registry ID
|
|
211
|
+
let registryPageOrder = null;
|
|
212
|
+
for (let i = 0; i < canvases.length; i++) {
|
|
213
|
+
const c = canvases[i];
|
|
214
|
+
let cId = null;
|
|
215
|
+
if (c.label) {
|
|
216
|
+
const label = c.label.none || c.label.en || c.label;
|
|
217
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
218
|
+
if (labelStr) {
|
|
219
|
+
const pathParts = window.location.pathname.split('/');
|
|
220
|
+
let docId = null;
|
|
221
|
+
for (let j = pathParts.length - 1; j >= 0; j--) {
|
|
222
|
+
if (pathParts[j] && pathParts[j] !== 'apps' && pathParts[j] !== 'exist') {
|
|
223
|
+
docId = pathParts[j];
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (docId) {
|
|
228
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
229
|
+
cId = `${docId}_${pageNum}.jpg`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (cId === registryState.id) {
|
|
234
|
+
registryPageOrder = i + 1;
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Only ignore if Tify is on the registry page (stale auto-trigger)
|
|
240
|
+
// If Tify is on a different page, allow navigation (user-initiated)
|
|
241
|
+
if (currentTifyPage === registryPageOrder) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Otherwise allow it - Tify is on a different page, might be user navigation
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this._initialPages = pageOrder;
|
|
251
|
+
const url = ev.detail.file || ev.detail.url;
|
|
252
|
+
if (url && url !== this.manifest) {
|
|
253
|
+
this.manifest = ev.detail.file;
|
|
254
|
+
this._initViewer();
|
|
255
|
+
// check if tify is already initialized
|
|
256
|
+
} else if (this._setPage) {
|
|
257
|
+
// Convert to array format that Tify expects
|
|
258
|
+
const pagesArray = Array.isArray(this._initialPages) ? this._initialPages : [this._initialPages];
|
|
259
|
+
this._setPage(pagesArray);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (ev.detail.coordinates) {
|
|
263
|
+
this._addOverlay(ev.detail.coordinates);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Subscribe to registry for URL changes (back/forward navigation)
|
|
269
|
+
if (!this.disableHistory) {
|
|
270
|
+
registry.subscribe(this, (state) => {
|
|
271
|
+
// Only handle URL changes after Tify is fully ready
|
|
272
|
+
// Initial load is handled in setInitialPage
|
|
273
|
+
if (this._initialLoadComplete) {
|
|
274
|
+
this._handleUrlChange(state);
|
|
55
275
|
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Initial URL state will be handled in _onTifyFullyReady
|
|
279
|
+
// to ensure Tify is fully initialized before loading from URL
|
|
56
280
|
}
|
|
57
281
|
|
|
58
|
-
|
|
59
|
-
|
|
282
|
+
// Subscribe to pb-navigate events if navigation is enabled
|
|
283
|
+
if (this.enableNavigation !== false) {
|
|
284
|
+
this.subscribeTo('pb-navigate', ev => {
|
|
285
|
+
if (!ev.detail || !ev.detail.direction) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const direction = ev.detail.direction;
|
|
290
|
+
|
|
291
|
+
// Check if tify is initialized
|
|
292
|
+
if (!this._tify || !this._tify.app) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Wait for tify to be ready if it's not yet
|
|
297
|
+
const root = this._getRootFromApp();
|
|
298
|
+
if (!root) {
|
|
299
|
+
this._pendingNavigation = direction;
|
|
300
|
+
if (this._tify.ready) {
|
|
301
|
+
this._tify.ready.then(() => {
|
|
302
|
+
// Wait a bit more for root to be available
|
|
303
|
+
const waitForRoot = () => {
|
|
304
|
+
const root = this._getRootFromApp();
|
|
305
|
+
if (root) {
|
|
306
|
+
if (this._pendingNavigation) {
|
|
307
|
+
const queuedDirection = this._pendingNavigation;
|
|
308
|
+
this._pendingNavigation = null;
|
|
309
|
+
this._handleNavigate(queuedDirection);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Retry after a short delay (max 10 seconds)
|
|
313
|
+
if (!this._navWaitStart) {
|
|
314
|
+
this._navWaitStart = Date.now();
|
|
315
|
+
}
|
|
316
|
+
const elapsed = Date.now() - this._navWaitStart;
|
|
317
|
+
if (elapsed < 10000) {
|
|
318
|
+
setTimeout(waitForRoot, 50);
|
|
319
|
+
} else {
|
|
320
|
+
// Try to proceed anyway
|
|
321
|
+
if (this._pendingNavigation) {
|
|
322
|
+
const queuedDirection = this._pendingNavigation;
|
|
323
|
+
this._pendingNavigation = null;
|
|
324
|
+
this._handleNavigate(queuedDirection);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
waitForRoot();
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this._handleNavigate(direction);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.signalReady();
|
|
340
|
+
}
|
|
60
341
|
|
|
61
|
-
|
|
342
|
+
disconnectedCallback() {
|
|
343
|
+
super.disconnectedCallback();
|
|
344
|
+
|
|
345
|
+
// Clean up Vue store watcher if it exists
|
|
346
|
+
if (this._vueStoreWatcher && typeof this._vueStoreWatcher === 'function') {
|
|
347
|
+
this._vueStoreWatcher();
|
|
348
|
+
this._vueStoreWatcher = null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Clean up polling interval if it exists
|
|
352
|
+
if (this._vueStoreWatcherInterval) {
|
|
353
|
+
clearInterval(this._vueStoreWatcherInterval);
|
|
354
|
+
this._vueStoreWatcherInterval = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Clean up Tify error watcher if it exists
|
|
358
|
+
if (this._tifyErrorWatcherInterval) {
|
|
359
|
+
clearInterval(this._tifyErrorWatcherInterval);
|
|
360
|
+
this._tifyErrorWatcherInterval = null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Cleanup is handled by registry automatically
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
firstUpdated() {
|
|
367
|
+
super.firstUpdated();
|
|
368
|
+
|
|
369
|
+
waitOnce('pb-page-ready', () => {
|
|
370
|
+
this._initViewer();
|
|
371
|
+
});
|
|
372
|
+
}
|
|
62
373
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
374
|
+
_initViewer() {
|
|
375
|
+
// Make sure a mount point exists even if called before connectedCallback
|
|
376
|
+
this._ensureContainer();
|
|
377
|
+
|
|
378
|
+
// Don't initialize if no manifest is provided
|
|
379
|
+
if (!this.manifest || this.manifest.trim() === '') {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Clean up existing Tify instance and watchers before creating a new one
|
|
384
|
+
if (this._tify) {
|
|
385
|
+
this._tify.destroy();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clean up Vue store watcher if it exists (prevents duplicate polling)
|
|
389
|
+
if (this._vueStoreWatcherInterval) {
|
|
390
|
+
clearInterval(this._vueStoreWatcherInterval);
|
|
391
|
+
this._vueStoreWatcherInterval = null;
|
|
392
|
+
}
|
|
393
|
+
if (this._vueStoreWatcher && typeof this._vueStoreWatcher === 'function') {
|
|
394
|
+
this._vueStoreWatcher();
|
|
395
|
+
this._vueStoreWatcher = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Clean up Tify error watcher if it exists
|
|
399
|
+
if (this._tifyErrorWatcherInterval) {
|
|
400
|
+
clearInterval(this._tifyErrorWatcherInterval);
|
|
401
|
+
this._tifyErrorWatcherInterval = null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const endpoint = this.getEndpoint();
|
|
406
|
+
|
|
407
|
+
// In component test environments, endpoint might be undefined
|
|
408
|
+
// Use a default endpoint for testing purposes
|
|
409
|
+
const effectiveEndpoint = endpoint || 'http://localhost:5173';
|
|
410
|
+
|
|
411
|
+
const manifestUrl = this.toAbsoluteURL(this.manifest, effectiveEndpoint);
|
|
412
|
+
|
|
413
|
+
// Only validate that we have a manifest URL - let Tify handle invalid URLs
|
|
414
|
+
if (!manifestUrl || manifestUrl.trim() === '') {
|
|
415
|
+
console.warn('<pb-tify> Invalid manifest URL:', this.manifest);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Store manifest URL for fallback fetching
|
|
420
|
+
this._manifestUrl = manifestUrl;
|
|
421
|
+
|
|
422
|
+
// Check URL for initial page BEFORE creating Tify
|
|
423
|
+
// This allows us to set the page immediately when Tify is ready
|
|
424
|
+
const state = registry.getState(this);
|
|
425
|
+
this._initialUrlPage = null;
|
|
426
|
+
if (state.id || state.root) {
|
|
427
|
+
// Store that we have a URL page - will resolve actual page number after Tify is ready
|
|
428
|
+
this._hasInitialUrlPage = true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Build tify options
|
|
432
|
+
const tifyOptions = {
|
|
433
|
+
manifestUrl: manifestUrl,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Set initial view if specified (e.g., 'toc' to show table of contents with double pages)
|
|
437
|
+
if (this.initialView) {
|
|
438
|
+
tifyOptions.view = this.initialView;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
this._tify = new Tify(tifyOptions);
|
|
442
|
+
|
|
443
|
+
// Try to listen to Tify's internal events if available
|
|
444
|
+
// Some versions of Tify might emit custom events when pages change
|
|
445
|
+
if (this._tify && this._tify.viewer && this._tify.viewer.$el) {
|
|
446
|
+
// Tify events are now handled via registry subscription
|
|
447
|
+
// No need to listen to individual Tify events
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Wait for ready, then try to get root, but proceed either way
|
|
451
|
+
// The viewer should display even without root - we only need root for navigation
|
|
452
|
+
if (this._tify && this._tify.ready) {
|
|
453
|
+
this._tify.ready.then(() => {
|
|
454
|
+
// Try to get root, but don't wait too long
|
|
455
|
+
const waitForRoot = () => {
|
|
456
|
+
if (this._tify && this._tify.app) {
|
|
457
|
+
// Try different ways to access the manifest/root data
|
|
458
|
+
const root = this._tify.app.$root || this._tify.app.root || this._tify.app.manifest || (this._tify.app.$data && this._tify.app.$data.root);
|
|
459
|
+
if (root) {
|
|
460
|
+
this._onTifyFullyReady();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Retry after a short delay (max 2 seconds - don't wait too long)
|
|
466
|
+
if (!this._rootWaitStart) {
|
|
467
|
+
this._rootWaitStart = Date.now();
|
|
468
|
+
}
|
|
469
|
+
const elapsed = Date.now() - this._rootWaitStart;
|
|
470
|
+
if (elapsed < 2000) {
|
|
471
|
+
setTimeout(waitForRoot, 50);
|
|
472
|
+
} else {
|
|
473
|
+
// Try to fetch manifest directly as fallback
|
|
474
|
+
this._fetchAndCacheManifest().then(() => {
|
|
475
|
+
// Proceed anyway - Tify should still work, we'll handle navigation differently
|
|
476
|
+
this._onTifyFullyReady();
|
|
477
|
+
}).catch(() => {
|
|
478
|
+
// Proceed even if fetch fails
|
|
479
|
+
this._onTifyFullyReady();
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
waitForRoot();
|
|
484
|
+
}).catch(error => {
|
|
485
|
+
// Tify's ready promise rejects when manifest loading fails
|
|
486
|
+
// This is the proper way to detect manifest errors
|
|
487
|
+
console.error('<pb-tify> Tify ready promise rejected:', error);
|
|
488
|
+
this._handleManifestError(error);
|
|
489
|
+
});
|
|
490
|
+
} else {
|
|
491
|
+
// If ready promise not available, proceed anyway after a short delay
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
this._onTifyFullyReady();
|
|
494
|
+
}, 500);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Also watch Tify's error store for errors that might not reject the ready promise
|
|
498
|
+
// This provides additional error detection
|
|
499
|
+
if (this._tify && this._tify.app) {
|
|
500
|
+
// Wait a bit for Tify to initialize, then set up error watcher
|
|
501
|
+
setTimeout(() => {
|
|
502
|
+
this._setupTifyErrorWatcher();
|
|
503
|
+
}, 100);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Mount Tify to the container
|
|
507
|
+
this._tify.mount(this._container);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error('<pb-tify> Failed to initialize Tify:', error);
|
|
510
|
+
this._handleManifestError(error);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async _onTifyFullyReady() {
|
|
515
|
+
// Clear any previous error messages
|
|
516
|
+
this._clearError();
|
|
517
|
+
|
|
518
|
+
// extend tify's setPage function to allow emitting an event
|
|
519
|
+
const { app } = this._tify;
|
|
520
|
+
|
|
521
|
+
// Check if app exists and has the necessary structure
|
|
522
|
+
if (!app) {
|
|
523
|
+
console.error('<pb-tify> Tify app is not available');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Check if app.pages exists - it may not be available in all Tify versions
|
|
528
|
+
const hasAppPages = app.pages !== undefined && app.pages !== null;
|
|
529
|
+
|
|
530
|
+
// Track the last known canvas ID for change detection
|
|
531
|
+
this._lastCanvasId = null;
|
|
532
|
+
|
|
533
|
+
// Also listen for clicks on thumbnail elements specifically
|
|
534
|
+
if (this._tify && this._tify.viewer) {
|
|
535
|
+
const viewer = this._tify.viewer;
|
|
536
|
+
|
|
537
|
+
// Wait a bit for the viewer to be fully rendered
|
|
538
|
+
setTimeout(() => {
|
|
539
|
+
// Find thumbnail container - Tify typically uses classes like 'tify-thumbnails' or similar
|
|
540
|
+
const thumbnailContainer = viewer.$el?.querySelector('.tify-thumbnails') ||
|
|
541
|
+
viewer.$el?.querySelector('[class*="thumbnail"]') ||
|
|
542
|
+
this._container?.querySelector('.tify-thumbnails') ||
|
|
543
|
+
this._container?.querySelector('[class*="thumbnail"]');
|
|
67
544
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
545
|
+
if (thumbnailContainer) {
|
|
546
|
+
// Helper function to find thumbnail index from click event
|
|
547
|
+
const findThumbnailIndex = (ev) => {
|
|
548
|
+
// First, try to extract from image URL - this is the most reliable method
|
|
549
|
+
if (ev.target.tagName === 'IMG' && ev.target.src) {
|
|
550
|
+
const src = ev.target.src;
|
|
551
|
+
// Try to match page number in URL (e.g., A-N-38_003.jpg -> page 3 -> index 2)
|
|
552
|
+
const match = src.match(/_(\d+)\.jpg/);
|
|
553
|
+
if (match) {
|
|
554
|
+
const pageNum = parseInt(match[1], 10);
|
|
555
|
+
if (!isNaN(pageNum) && pageNum > 0) {
|
|
556
|
+
// Page numbers in URLs are usually 1-indexed, convert to 0-indexed
|
|
557
|
+
return pageNum - 1;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Fallback: find the actual list item that contains the clicked element
|
|
563
|
+
// Walk up from the clicked element to find the <li> parent
|
|
564
|
+
let element = ev.target;
|
|
565
|
+
let listItem = null;
|
|
566
|
+
|
|
567
|
+
while (element && element !== thumbnailContainer) {
|
|
568
|
+
if (element.tagName === 'LI') {
|
|
569
|
+
listItem = element;
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
element = element.parentElement;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (listItem) {
|
|
576
|
+
// Get all list items in the container (not the container itself)
|
|
577
|
+
const allListItems = Array.from(thumbnailContainer.querySelectorAll('li'));
|
|
578
|
+
const index = allListItems.indexOf(listItem);
|
|
579
|
+
if (index >= 0) {
|
|
580
|
+
return index;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Last resort: try to find by data attribute on any parent
|
|
585
|
+
element = ev.target;
|
|
586
|
+
while (element && element !== thumbnailContainer) {
|
|
587
|
+
const dataIndex = element.getAttribute('data-index') ||
|
|
588
|
+
element.getAttribute('data-page') ||
|
|
589
|
+
element.getAttribute('data-order');
|
|
590
|
+
if (dataIndex !== null) {
|
|
591
|
+
const parsed = parseInt(dataIndex, 10);
|
|
592
|
+
if (!isNaN(parsed) && parsed >= 0) {
|
|
593
|
+
return parsed;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
element = element.parentElement;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return -1;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Listen for clicks on thumbnail items
|
|
603
|
+
// Use bubble phase (not capture) so Tify handles the click first
|
|
604
|
+
thumbnailContainer.addEventListener('click', (ev) => {
|
|
605
|
+
const thumbnailIndex = findThumbnailIndex(ev);
|
|
606
|
+
|
|
607
|
+
if (thumbnailIndex >= 0) {
|
|
608
|
+
// Get the root and canvases
|
|
609
|
+
const root = this._getRootFromApp();
|
|
610
|
+
if (root) {
|
|
611
|
+
const canvases = this._getCanvases(root);
|
|
612
|
+
if (thumbnailIndex < canvases.length) {
|
|
613
|
+
const newPage = thumbnailIndex + 1; // Convert to 1-indexed
|
|
614
|
+
const canvas = canvases[thumbnailIndex];
|
|
615
|
+
|
|
616
|
+
if (canvas) {
|
|
617
|
+
// Extract target page ID
|
|
618
|
+
const { rendering } = canvas;
|
|
619
|
+
let targetId = null;
|
|
620
|
+
if (rendering && rendering.length > 0) {
|
|
621
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
622
|
+
if (renderingId) {
|
|
623
|
+
try {
|
|
624
|
+
const url = new URL(renderingId);
|
|
625
|
+
targetId = url.searchParams.get('id');
|
|
626
|
+
} catch (e) {
|
|
627
|
+
// Ignore
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (!targetId && canvas.label) {
|
|
632
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
633
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
634
|
+
if (labelStr) {
|
|
635
|
+
const pathParts = window.location.pathname.split('/');
|
|
636
|
+
let docId = null;
|
|
637
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
638
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
639
|
+
docId = pathParts[i];
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (docId) {
|
|
644
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
645
|
+
targetId = `${docId}_${pageNum}.jpg`;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Set navigation state for thumbnail navigation
|
|
651
|
+
this._setNavigationState('thumbnail', newPage, targetId);
|
|
652
|
+
|
|
653
|
+
// Let Tify handle the click first, then wait for it to actually change pages
|
|
654
|
+
// BEFORE committing to registry. This prevents checkPageChange from seeing a mismatch.
|
|
655
|
+
let attempts = 0;
|
|
656
|
+
const maxAttempts = 10;
|
|
657
|
+
const waitForTifyThenUpdate = async () => {
|
|
658
|
+
attempts++;
|
|
659
|
+
|
|
660
|
+
// Try to get the current page from Tify's viewer
|
|
661
|
+
let tifyCurrentPage = null;
|
|
662
|
+
if (this._tify && this._tify.viewer) {
|
|
663
|
+
const viewer = this._tify.viewer;
|
|
664
|
+
if (typeof viewer.currentPage === 'function') {
|
|
665
|
+
try {
|
|
666
|
+
const page = viewer.currentPage();
|
|
667
|
+
if (typeof page === 'number') {
|
|
668
|
+
tifyCurrentPage = page + 1; // Convert to 1-indexed
|
|
669
|
+
}
|
|
670
|
+
} catch (e) {
|
|
671
|
+
// Ignore
|
|
672
|
+
}
|
|
673
|
+
} else if (viewer._sequenceIndex !== undefined) {
|
|
674
|
+
tifyCurrentPage = viewer._sequenceIndex + 1;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Only proceed when Tify has actually changed to the target page
|
|
679
|
+
if (tifyCurrentPage === newPage) {
|
|
680
|
+
// Tify has changed - now it's safe to update registry
|
|
681
|
+
// Get the current canvas
|
|
682
|
+
const root = this._getRootFromApp();
|
|
683
|
+
if (root) {
|
|
684
|
+
const canvases = this._getCanvases(root);
|
|
685
|
+
const targetIndex = tifyCurrentPage - 1;
|
|
686
|
+
if (targetIndex >= 0 && targetIndex < canvases.length) {
|
|
687
|
+
const currentCanvas = canvases[targetIndex];
|
|
688
|
+
if (currentCanvas) {
|
|
689
|
+
// Extract the ID we're about to commit so _handleUrlChange can skip it
|
|
690
|
+
// This must be done BEFORE calling _updateUrlFromPage
|
|
691
|
+
let canvasId = null;
|
|
692
|
+
const { rendering } = currentCanvas;
|
|
693
|
+
if (rendering && rendering.length > 0) {
|
|
694
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
695
|
+
if (renderingId) {
|
|
696
|
+
try {
|
|
697
|
+
const url = new URL(renderingId, window.location.origin);
|
|
698
|
+
canvasId = url.searchParams.get('id');
|
|
699
|
+
} catch (e) {
|
|
700
|
+
// Ignore
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!canvasId && currentCanvas.label) {
|
|
705
|
+
const label = currentCanvas.label.none || currentCanvas.label.en || currentCanvas.label;
|
|
706
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
707
|
+
if (labelStr) {
|
|
708
|
+
const pathParts = window.location.pathname.split('/');
|
|
709
|
+
let docId = null;
|
|
710
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
711
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
712
|
+
docId = pathParts[i];
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (docId) {
|
|
717
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
718
|
+
canvasId = `${docId}_${pageNum}.jpg`;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// Update navigation state with actual canvas ID
|
|
723
|
+
if (canvasId) {
|
|
724
|
+
this._setNavigationState('thumbnail', tifyCurrentPage, canvasId);
|
|
725
|
+
}
|
|
726
|
+
// Now update registry - Tify is already on the correct page
|
|
727
|
+
// This prevents checkPageChange from seeing a mismatch
|
|
728
|
+
await this._updateUrlFromPage(currentCanvas, true);
|
|
729
|
+
// Also emit refresh explicitly to ensure pb-view gets notified
|
|
730
|
+
this._emitPbRefresh(currentCanvas);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// Fallback: use the original canvas
|
|
736
|
+
await this._updateUrlFromPage(canvas, true);
|
|
737
|
+
this._emitPbRefresh(canvas);
|
|
738
|
+
} else if (attempts < maxAttempts) {
|
|
739
|
+
// Tify hasn't changed yet, retry after a short delay
|
|
740
|
+
setTimeout(waitForTifyThenUpdate, 100);
|
|
741
|
+
} else {
|
|
742
|
+
// Give up after max attempts - update anyway
|
|
743
|
+
console.warn('[pb-tify] thumbnail click: max attempts reached, updating registry anyway', {
|
|
744
|
+
tifyCurrentPage,
|
|
745
|
+
newPage,
|
|
746
|
+
attempts,
|
|
747
|
+
maxAttempts
|
|
748
|
+
});
|
|
749
|
+
await this._updateUrlFromPage(canvas, true);
|
|
750
|
+
this._emitPbRefresh(canvas);
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// Start checking after a short initial delay to let Tify start changing
|
|
755
|
+
setTimeout(waitForTifyThenUpdate, 150);
|
|
756
|
+
|
|
757
|
+
// Re-enable page monitoring after a delay to let Tify settle
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
// Mark navigation complete for post-navigation protection
|
|
760
|
+
const committedId = this._navigationState?.targetId || targetId;
|
|
761
|
+
if (committedId) {
|
|
762
|
+
this._markNavigationComplete(committedId);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Clear navigation state after a delay
|
|
766
|
+
setTimeout(() => {
|
|
767
|
+
this._clearNavigationState();
|
|
768
|
+
}, 500);
|
|
769
|
+
}, 2000);
|
|
770
|
+
}
|
|
73
771
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}, false); // Use bubble phase - let Tify handle click first
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Monitor Tify's page changes to update registry
|
|
778
|
+
// This catches all navigation methods (buttons, keyboard, etc.)
|
|
779
|
+
let lastPage = null;
|
|
780
|
+
let lastHash = window.location.hash;
|
|
781
|
+
const checkPageChange = async () => {
|
|
782
|
+
// Don't monitor during initial load
|
|
783
|
+
if (!this._initialLoadComplete) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!this._tify || !this._tify.viewer) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check if navigation is active (any source)
|
|
792
|
+
if (this._isNavigationActive()) {
|
|
793
|
+
return; // Navigation in progress, don't interfere
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Check if we recently committed (post-navigation protection)
|
|
797
|
+
if (this._hasRecentCommit()) {
|
|
798
|
+
return; // Recently committed, let it settle
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
let currentPage = null;
|
|
802
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
803
|
+
try {
|
|
804
|
+
const page = this._tify.viewer.currentPage();
|
|
805
|
+
if (typeof page === 'number') {
|
|
806
|
+
currentPage = page + 1; // Convert to 1-indexed
|
|
807
|
+
}
|
|
808
|
+
} catch (e) {
|
|
809
|
+
// Ignore
|
|
810
|
+
}
|
|
811
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
812
|
+
const seqIndex = this._tify.viewer._sequenceIndex;
|
|
813
|
+
if (typeof seqIndex === 'number') {
|
|
814
|
+
currentPage = seqIndex + 1; // Convert to 1-indexed
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Also check hash change (Tify might update hash directly)
|
|
819
|
+
const currentHash = window.location.hash;
|
|
820
|
+
const hashChanged = currentHash !== lastHash;
|
|
821
|
+
|
|
822
|
+
// If page changed or hash changed, check if URL already matches before updating
|
|
823
|
+
if ((currentPage && currentPage !== lastPage) || hashChanged) {
|
|
824
|
+
// Check if URL already has this page
|
|
825
|
+
const state = registry.getState(this);
|
|
826
|
+
const root = this._getRootFromApp();
|
|
827
|
+
if (root) {
|
|
828
|
+
const canvases = this._getCanvases(root);
|
|
829
|
+
if (currentPage >= 1 && currentPage <= canvases.length) {
|
|
830
|
+
const canvas = canvases[currentPage - 1];
|
|
831
|
+
if (canvas) {
|
|
832
|
+
// Check if URL already matches this canvas
|
|
833
|
+
let urlMatches = false;
|
|
834
|
+
if (state.id) {
|
|
835
|
+
const { rendering } = canvas;
|
|
836
|
+
if (rendering && rendering.length > 0) {
|
|
837
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
838
|
+
if (renderingId) {
|
|
839
|
+
try {
|
|
840
|
+
const url = new URL(renderingId);
|
|
841
|
+
const canvasId = url.searchParams.get('id');
|
|
842
|
+
if (canvasId === state.id) {
|
|
843
|
+
urlMatches = true;
|
|
844
|
+
}
|
|
845
|
+
} catch (e) {
|
|
846
|
+
// Ignore
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Only update if URL doesn't match AND we're not navigating to this page
|
|
853
|
+
// Check if this is the target page we're navigating to
|
|
854
|
+
let isTargetPage = false;
|
|
855
|
+
if (this._targetPageId) {
|
|
856
|
+
const { rendering } = canvas;
|
|
857
|
+
if (rendering && rendering.length > 0) {
|
|
858
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
859
|
+
if (renderingId) {
|
|
860
|
+
try {
|
|
861
|
+
const url = new URL(renderingId);
|
|
862
|
+
const canvasId = url.searchParams.get('id');
|
|
863
|
+
if (canvasId === this._targetPageId) {
|
|
864
|
+
isTargetPage = true;
|
|
865
|
+
}
|
|
866
|
+
} catch (e) {
|
|
867
|
+
// Ignore
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Only update if URL doesn't match AND this isn't our target page (prevents resets)
|
|
874
|
+
// Also check if we just committed to a different page - if so, wait for Tify to catch up
|
|
875
|
+
const currentState = registry.getState(this);
|
|
876
|
+
const justCommitted = this._lastCommittedId && currentState.id === this._lastCommittedId;
|
|
877
|
+
|
|
878
|
+
// Also check if we're in the middle of a commit (registry might not have updated yet)
|
|
879
|
+
const isCommitting = this._isCommitting;
|
|
880
|
+
|
|
881
|
+
// Don't update if we have a target page ID set (we're navigating to it)
|
|
882
|
+
const hasTargetPage = !!this._targetPageId;
|
|
883
|
+
|
|
884
|
+
// Don't update if thumbnail navigation is in progress (even if flag hasn't been checked yet)
|
|
885
|
+
const thumbnailNav = this._thumbnailNavigationInProgress;
|
|
886
|
+
|
|
887
|
+
// Don't update if the current page matches what we just committed (we're navigating to it)
|
|
888
|
+
// Check if currentState.id matches _lastCommittedId - if so, we just committed this, don't reset
|
|
889
|
+
const matchesLastCommit = this._lastCommittedId &&
|
|
890
|
+
currentState.id === this._lastCommittedId &&
|
|
891
|
+
currentState.id !== null;
|
|
892
|
+
|
|
893
|
+
// Also check if we're trying to update to a page that matches what we just committed
|
|
894
|
+
// This prevents checkPageChange from resetting when we just committed a page change via interception
|
|
895
|
+
let canvasMatchesLastCommit = false;
|
|
896
|
+
if (this._lastCommittedId && canvas) {
|
|
897
|
+
const { rendering } = canvas;
|
|
898
|
+
if (rendering && rendering.length > 0) {
|
|
899
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
900
|
+
if (renderingId) {
|
|
901
|
+
try {
|
|
902
|
+
const url = new URL(renderingId, window.location.href);
|
|
903
|
+
const canvasId = url.searchParams.get('id');
|
|
904
|
+
if (canvasId === this._lastCommittedId) {
|
|
905
|
+
canvasMatchesLastCommit = true;
|
|
906
|
+
}
|
|
907
|
+
} catch (e) {
|
|
908
|
+
// Ignore
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
// Also check by generating pageId from canvas label
|
|
913
|
+
if (!canvasMatchesLastCommit && canvas.label) {
|
|
914
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
915
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
916
|
+
if (labelStr) {
|
|
917
|
+
const pathParts = window.location.pathname.split('/');
|
|
918
|
+
let docId = null;
|
|
919
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
920
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
921
|
+
docId = pathParts[i];
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (docId) {
|
|
926
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
927
|
+
const pageId = `${docId}_${pageNum}.jpg`;
|
|
928
|
+
if (pageId === this._lastCommittedId) {
|
|
929
|
+
canvasMatchesLastCommit = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// CRITICAL: Don't commit if registry has a newer page than what Tify is showing
|
|
937
|
+
// This prevents checkPageChange from overwriting a recent commit with an older page
|
|
938
|
+
// Extract page numbers to compare
|
|
939
|
+
let registryPageNum = null;
|
|
940
|
+
let canvasPageNum = null;
|
|
941
|
+
if (currentState.id) {
|
|
942
|
+
const match = currentState.id.match(/_(\d{2,3})\./);
|
|
943
|
+
if (match) {
|
|
944
|
+
registryPageNum = parseInt(match[1], 10);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (canvas && canvas.label) {
|
|
948
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
949
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
950
|
+
if (labelStr) {
|
|
951
|
+
canvasPageNum = parseInt(labelStr, 10);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// If registry has a newer page (higher number), don't overwrite it with an older page
|
|
955
|
+
// ALSO: If _lastCommittedId exists and has a newer page, don't overwrite it
|
|
956
|
+
let wouldDowngrade = registryPageNum !== null && canvasPageNum !== null &&
|
|
957
|
+
canvasPageNum < registryPageNum;
|
|
958
|
+
|
|
959
|
+
// Additional check: if we recently committed a newer page, don't overwrite it
|
|
960
|
+
if (!wouldDowngrade && this._lastCommittedId) {
|
|
961
|
+
const lastCommitMatch = this._lastCommittedId.match(/_(\d{2,3})\./);
|
|
962
|
+
if (lastCommitMatch) {
|
|
963
|
+
const lastCommitPageNum = parseInt(lastCommitMatch[1], 10);
|
|
964
|
+
if (canvasPageNum !== null && canvasPageNum < lastCommitPageNum) {
|
|
965
|
+
wouldDowngrade = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
if (!urlMatches && !isTargetPage && !justCommitted && !isCommitting && !hasTargetPage && !thumbnailNav && !matchesLastCommit && !canvasMatchesLastCommit && !wouldDowngrade) {
|
|
971
|
+
// Extract target page ID for Tify button clicks
|
|
972
|
+
const { rendering } = canvas;
|
|
973
|
+
let targetId = null;
|
|
974
|
+
if (rendering && rendering.length > 0) {
|
|
975
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
976
|
+
if (renderingId) {
|
|
977
|
+
try {
|
|
978
|
+
const url = new URL(renderingId);
|
|
979
|
+
targetId = url.searchParams.get('id');
|
|
980
|
+
} catch (e) {
|
|
981
|
+
// Ignore
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Set navigation state for button/keyboard navigation
|
|
987
|
+
this._setNavigationState('user', currentPage, targetId);
|
|
988
|
+
|
|
989
|
+
// Force update to bypass registry update check (Tify buttons are user actions)
|
|
990
|
+
await this._updateUrlFromPage(canvas, true);
|
|
991
|
+
// Also emit refresh directly to ensure transcription updates
|
|
992
|
+
this._emitPbRefresh(canvas);
|
|
993
|
+
|
|
994
|
+
// Mark navigation complete and clear after a delay
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
if (targetId) {
|
|
997
|
+
this._markNavigationComplete(targetId);
|
|
998
|
+
}
|
|
999
|
+
setTimeout(() => {
|
|
1000
|
+
this._clearNavigationState();
|
|
1001
|
+
}, 500);
|
|
1002
|
+
}, 300);
|
|
1003
|
+
}
|
|
81
1004
|
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
lastPage = currentPage;
|
|
1008
|
+
lastHash = currentHash;
|
|
1009
|
+
} else if (currentPage) {
|
|
1010
|
+
lastPage = currentPage;
|
|
1011
|
+
lastHash = currentHash;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// Check for page changes more frequently to catch Tify button clicks
|
|
1016
|
+
setInterval(checkPageChange, 200);
|
|
1017
|
+
|
|
1018
|
+
}, 500); // Wait 500ms for viewer to render
|
|
1019
|
+
}
|
|
82
1020
|
|
|
83
|
-
|
|
84
|
-
|
|
1021
|
+
// Use tify's setPage method directly, or app.setPage if available
|
|
1022
|
+
// According to Tify API: setPage(pageOrPages) accepts 1-based integer or array
|
|
1023
|
+
// Returns: array of current pages or false if invalid
|
|
1024
|
+
// Note: Actual implementation throws RangeError for invalid pages
|
|
1025
|
+
const tifySetPage = this._tify.setPage;
|
|
1026
|
+
const appSetPage = app.setPage;
|
|
1027
|
+
|
|
1028
|
+
// Helper function to wrap setPage with proper error handling and synchronization
|
|
1029
|
+
const createSetPageWrapper = (originalSetPage, context) => {
|
|
1030
|
+
return async (pageOrPages) => {
|
|
1031
|
+
// Prevent duplicate page setting (within 200ms)
|
|
1032
|
+
const pagesArray = Array.isArray(pageOrPages) ? pageOrPages : [pageOrPages];
|
|
1033
|
+
const page = Number(pagesArray[0]);
|
|
1034
|
+
const now = Date.now();
|
|
1035
|
+
|
|
1036
|
+
if (this._lastSetPage === page && (now - this._lastSetPageTime) < 200) {
|
|
1037
|
+
return; // Skip duplicate setPage call
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Get canvases using helper that supports both IIIF 2.0 and 3.0
|
|
1041
|
+
const root = this._getRootFromApp();
|
|
1042
|
+
const canvases = this._getCanvases(root);
|
|
1043
|
+
|
|
1044
|
+
// If canvases aren't available yet, queue the page navigation
|
|
1045
|
+
if (canvases.length === 0) {
|
|
1046
|
+
this._pendingPage = pagesArray;
|
|
1047
|
+
// Wait a bit and try again (max 5 seconds)
|
|
1048
|
+
if (!this._canvasWaitStart) {
|
|
1049
|
+
this._canvasWaitStart = Date.now();
|
|
1050
|
+
}
|
|
1051
|
+
const elapsed = Date.now() - this._canvasWaitStart;
|
|
1052
|
+
if (elapsed < 5000) {
|
|
1053
|
+
setTimeout(() => {
|
|
1054
|
+
if (this._pendingPage && this._setPage) {
|
|
1055
|
+
const queuedPages = this._pendingPage;
|
|
1056
|
+
this._pendingPage = null;
|
|
1057
|
+
this._setPage(queuedPages);
|
|
1058
|
+
}
|
|
1059
|
+
}, 100);
|
|
1060
|
+
} else {
|
|
1061
|
+
this._pendingPage = null;
|
|
1062
|
+
this._canvasWaitStart = null;
|
|
85
1063
|
}
|
|
1064
|
+
return false; // Return false to indicate failure (matches Tify API)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Reset canvas wait timer if we have canvases now
|
|
1068
|
+
this._canvasWaitStart = null;
|
|
1069
|
+
|
|
1070
|
+
// Call original setPage - let Tify validate the page number
|
|
1071
|
+
// Tify will throw RangeError if invalid, or return array/false per API
|
|
1072
|
+
try {
|
|
1073
|
+
const result = originalSetPage.call(context, pageOrPages);
|
|
1074
|
+
|
|
1075
|
+
// Check return value: Tify API says it returns array or false
|
|
1076
|
+
if (result === false) {
|
|
1077
|
+
// Invalid page - Tify rejected it
|
|
1078
|
+
console.warn('<pb-tify> setPage returned false - invalid page:', pageOrPages);
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Success - result is array of current pages
|
|
1083
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
1084
|
+
const actualPage = result[0]; // Get first page from result
|
|
1085
|
+
|
|
1086
|
+
// Check if page actually changed
|
|
1087
|
+
const pageChanged = this._currentPage !== actualPage;
|
|
1088
|
+
this._currentPage = actualPage;
|
|
1089
|
+
this._lastSetPage = actualPage;
|
|
1090
|
+
this._lastSetPageTime = now;
|
|
1091
|
+
|
|
1092
|
+
const canvas = canvases[actualPage - 1];
|
|
1093
|
+
if (canvas) {
|
|
1094
|
+
this._lastCanvasId = canvas.id || canvas['@id'];
|
|
1095
|
+
|
|
1096
|
+
// If not navigation is active for 'url' source, this is a user-initiated navigation
|
|
1097
|
+
// (Tify buttons, keyboard, thumbnails, etc.) - always update registry and emit refresh
|
|
1098
|
+
const isUrlNavigation = this._isNavigationActive('url');
|
|
1099
|
+
if (!isUrlNavigation && pageChanged) {
|
|
1100
|
+
// Update registry and emit refresh (force=true ensures commit)
|
|
1101
|
+
await this._updateUrlFromPage(canvas, true);
|
|
1102
|
+
}
|
|
1103
|
+
// If URL navigation is active, don't update registry (would cause loop)
|
|
86
1104
|
}
|
|
87
|
-
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return result; // Return Tify's result (array or false)
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
// Tify throws RangeError for invalid pages
|
|
1110
|
+
if (error instanceof RangeError) {
|
|
1111
|
+
console.warn('<pb-tify> setPage RangeError - invalid page:', pageOrPages, error.message);
|
|
1112
|
+
return false; // Return false to match API behavior
|
|
1113
|
+
}
|
|
1114
|
+
// Other errors should be logged
|
|
1115
|
+
console.error('<pb-tify> Error calling setPage:', error);
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// Determine which setPage to use and wrap it
|
|
1122
|
+
if (typeof tifySetPage === 'function') {
|
|
1123
|
+
// Store original function
|
|
1124
|
+
const originalSetPage = tifySetPage.bind(this._tify);
|
|
1125
|
+
|
|
1126
|
+
// Wrap Tify's setPage
|
|
1127
|
+
this._tify.setPage = createSetPageWrapper(originalSetPage, this._tify);
|
|
1128
|
+
|
|
1129
|
+
this._setPage = (pages) => {
|
|
1130
|
+
// By the time _setPage is set up, Tify should already be ready
|
|
1131
|
+
// Call setPage directly - if Tify isn't ready, it will handle the error
|
|
1132
|
+
try {
|
|
1133
|
+
this._tify.setPage(pages);
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
// If Tify isn't ready yet, wait for it
|
|
1136
|
+
if (this._tify.ready) {
|
|
1137
|
+
this._tify.ready.then(() => {
|
|
1138
|
+
this._tify.setPage(pages);
|
|
1139
|
+
}).catch(() => {
|
|
1140
|
+
// Ignore errors
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
} else if (typeof appSetPage === 'function') {
|
|
1146
|
+
// Store original function
|
|
1147
|
+
const originalSetPage = appSetPage.bind(app);
|
|
1148
|
+
|
|
1149
|
+
// Wrap app's setPage
|
|
1150
|
+
app.setPage = createSetPageWrapper(originalSetPage, app);
|
|
1151
|
+
|
|
1152
|
+
this._setPage = (pages) => {
|
|
1153
|
+
// By the time _setPage is set up, Tify should already be ready
|
|
1154
|
+
// Call setPage directly - if Tify isn't ready, it will handle the error
|
|
1155
|
+
try {
|
|
1156
|
+
app.setPage(pages);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
// If Tify isn't ready yet, wait for it
|
|
1159
|
+
if (this._tify.ready) {
|
|
1160
|
+
this._tify.ready.then(() => {
|
|
1161
|
+
app.setPage(pages);
|
|
1162
|
+
}).catch(() => {
|
|
1163
|
+
// Ignore errors
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
};
|
|
1168
|
+
} else {
|
|
1169
|
+
// Fallback: create a simple wrapper that at least tracks the page
|
|
1170
|
+
this._setPage = (pages) => {
|
|
1171
|
+
const page = Array.isArray(pages) ? pages[0] : pages;
|
|
1172
|
+
this._currentPage = page;
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Set up Vue store watcher for immediate page change detection (Option 1)
|
|
1177
|
+
// This provides event-driven detection instead of relying solely on polling
|
|
1178
|
+
this._setupVueStoreWatcher(app);
|
|
1179
|
+
|
|
1180
|
+
// open initial page if set earlier via pb-load-facsimile event or from URL
|
|
1181
|
+
// Do this after setting up the override so _currentPage gets tracked
|
|
1182
|
+
// Tify expects pages as an array, and we need to ensure manifest is loaded
|
|
1183
|
+
const setInitialPage = () => {
|
|
1184
|
+
const root = this._getRootFromApp();
|
|
1185
|
+
if (!root) {
|
|
1186
|
+
setTimeout(setInitialPage, 100);
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const canvases = this._getCanvases(root);
|
|
1191
|
+
const canvasCount = canvases.length;
|
|
1192
|
+
|
|
1193
|
+
// Don't try to set page if there are no canvases yet
|
|
1194
|
+
if (canvasCount === 0) {
|
|
1195
|
+
// Wait a bit and try again
|
|
1196
|
+
setTimeout(setInitialPage, 100);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Read initial page from registry (like pb-view does)
|
|
1201
|
+
const state = registry.getState(this);
|
|
1202
|
+
let initialPage = null;
|
|
1203
|
+
|
|
1204
|
+
// PRIORITY 1: Check hash first (most reliable - e.g., #A-N-38_004.jpg)
|
|
1205
|
+
const hash = window.location.hash;
|
|
1206
|
+
if (hash) {
|
|
1207
|
+
const hashMatch = hash.match(/_(\d{2,3})\./);
|
|
1208
|
+
if (hashMatch) {
|
|
1209
|
+
const pageNum = parseInt(hashMatch[1], 10);
|
|
1210
|
+
if (pageNum >= 1 && pageNum <= canvasCount) {
|
|
1211
|
+
initialPage = pageNum;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// PRIORITY 2: Try to find page from registry state.id
|
|
1217
|
+
if (!initialPage && state.id) {
|
|
1218
|
+
// Find canvas by id - check both rendering URL id and canvas label
|
|
1219
|
+
for (let i = 0; i < canvases.length; i++) {
|
|
1220
|
+
const canvas = canvases[i];
|
|
1221
|
+
|
|
1222
|
+
// First check rendering URL id parameter
|
|
1223
|
+
const { rendering } = canvas;
|
|
1224
|
+
if (rendering && rendering.length > 0) {
|
|
1225
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
1226
|
+
if (renderingId) {
|
|
1227
|
+
try {
|
|
1228
|
+
const url = new URL(renderingId);
|
|
1229
|
+
const canvasId = url.searchParams.get('id');
|
|
1230
|
+
if (canvasId === state.id) {
|
|
1231
|
+
initialPage = i + 1;
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
// Ignore
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Also check canvas label for id match (e.g., A-N-38_002.jpg matches A-N-38_002.jpg)
|
|
1241
|
+
if (canvas.label) {
|
|
1242
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
1243
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
1244
|
+
if (labelStr) {
|
|
1245
|
+
// Check if state.id contains the label (e.g., "A-N-38_002.jpg" contains "002")
|
|
1246
|
+
// Or if label matches the id pattern
|
|
1247
|
+
if (state.id === labelStr || state.id.includes(labelStr) || labelStr.includes(state.id)) {
|
|
1248
|
+
initialPage = i + 1;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// If not in registry, check if set via pb-load-facsimile event
|
|
1257
|
+
if (!initialPage && this._initialPages) {
|
|
1258
|
+
const pagesArray = Array.isArray(this._initialPages) ? this._initialPages : [this._initialPages];
|
|
1259
|
+
initialPage = Number(pagesArray[0]);
|
|
1260
|
+
if (isNaN(initialPage) || initialPage < 1) {
|
|
1261
|
+
initialPage = 1;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Default to page 1
|
|
1266
|
+
if (!initialPage) {
|
|
1267
|
+
initialPage = 1;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Ensure page is within range
|
|
1271
|
+
const validPage = Math.max(1, Math.min(initialPage, canvasCount));
|
|
1272
|
+
|
|
1273
|
+
// Check if we found a page from URL (hash or state.id)
|
|
1274
|
+
// If hash is present OR state.id is set, we should commit to normalize the URL
|
|
1275
|
+
// This ensures shareable URLs have id in both query params and hash
|
|
1276
|
+
const hasUrlPage = (hash && initialPage === validPage) || (state.id && initialPage === validPage);
|
|
1277
|
+
const hasHashOrId = (hash && hash.length > 1) || (state.id && state.id.length > 0);
|
|
1278
|
+
|
|
1279
|
+
// Always commit if we have a hash or state.id to normalize the URL
|
|
1280
|
+
// This ensures shareable URLs have id in both query params and hash
|
|
1281
|
+
// We commit even if initialPage doesn't match validPage, as long as we have a hash/id
|
|
1282
|
+
if (hasUrlPage || hasHashOrId) {
|
|
1283
|
+
// Loading from URL - set page and wait for Tify to actually change
|
|
1284
|
+
// Set navigation state for URL-based initial load
|
|
1285
|
+
this._setNavigationState('url', validPage, state.id);
|
|
1286
|
+
|
|
1287
|
+
// Ensure URL has all three components (id, root, hash) for shareable URLs FIRST
|
|
1288
|
+
// Do this BEFORE setting the page so the URL is correct immediately
|
|
1289
|
+
// The registry may have read id from hash, but we need to commit it back
|
|
1290
|
+
// to ensure id is in query params AND hash
|
|
1291
|
+
// Use the page we calculated, or fall back to validPage
|
|
1292
|
+
const targetPage = initialPage > 0 ? initialPage : validPage;
|
|
1293
|
+
const canvas = canvases[targetPage - 1] || canvases[validPage - 1];
|
|
1294
|
+
if (canvas && (state.id || hash)) {
|
|
1295
|
+
// Directly commit to ensure id is in query params (registry will also set hash)
|
|
1296
|
+
// This ensures shareable URLs have id in both query params and hash
|
|
1297
|
+
const idToCommit = state.id || (hash ? hash.substring(1) : null);
|
|
1298
|
+
|
|
1299
|
+
if (idToCommit) {
|
|
1300
|
+
// Build commit state - only include root if it exists (don't set to null)
|
|
1301
|
+
const commitState = { id: idToCommit };
|
|
1302
|
+
if (state.root) {
|
|
1303
|
+
commitState.root = state.root;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// Use replace to avoid adding to history during initial load
|
|
1307
|
+
// Do this IMMEDIATELY, not in a setTimeout, so the URL is correct right away
|
|
1308
|
+
registry.replace(this, commitState);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// Now set the page after URL is committed
|
|
1313
|
+
this._setPage([validPage]);
|
|
1314
|
+
this._currentPage = validPage;
|
|
1315
|
+
|
|
1316
|
+
// Also call _updateUrlFromPage to fetch root if missing and ensure all components are present
|
|
1317
|
+
// This will update the URL with root if it wasn't available initially
|
|
1318
|
+
// Use async IIFE to properly await the async function
|
|
1319
|
+
(async () => {
|
|
1320
|
+
await this._updateUrlFromPage(canvas);
|
|
1321
|
+
})();
|
|
1322
|
+
|
|
1323
|
+
// Wait for Tify to actually change pages, then emit refresh
|
|
1324
|
+
const verifyAndEmit = (attempts = 0) => {
|
|
1325
|
+
// CRITICAL: Check if user has already navigated to a different page
|
|
1326
|
+
// If so, skip emitting pb-refresh to prevent overwriting user navigation
|
|
1327
|
+
const currentRegistryState = registry.getState(this);
|
|
1328
|
+
if (currentRegistryState.id) {
|
|
1329
|
+
// Try to extract page number from registry state
|
|
1330
|
+
const registryPageMatch = currentRegistryState.id.match(/_(\d{2,3})\./);
|
|
1331
|
+
if (registryPageMatch) {
|
|
1332
|
+
const registryPageNum = parseInt(registryPageMatch[1], 10);
|
|
1333
|
+
// If registry has a different page than what we're trying to set, user has navigated
|
|
1334
|
+
if (registryPageNum !== validPage) {
|
|
1335
|
+
this._clearNavigationState();
|
|
1336
|
+
this._initialLoadComplete = true;
|
|
1337
|
+
return; // Don't emit - user has already navigated
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Check if Tify is on the correct page
|
|
1343
|
+
let tifyPage = null;
|
|
1344
|
+
if (this._tify && this._tify.viewer) {
|
|
1345
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
1346
|
+
try {
|
|
1347
|
+
const page = this._tify.viewer.currentPage();
|
|
1348
|
+
if (typeof page === 'number') {
|
|
1349
|
+
tifyPage = page + 1;
|
|
1350
|
+
}
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
// Ignore
|
|
1353
|
+
}
|
|
1354
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
1355
|
+
const seqIndex = this._tify.viewer._sequenceIndex;
|
|
1356
|
+
if (typeof seqIndex === 'number') {
|
|
1357
|
+
tifyPage = seqIndex + 1;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (tifyPage === validPage) {
|
|
1363
|
+
// Tify is on the correct page
|
|
1364
|
+
this._clearNavigationState();
|
|
1365
|
+
this._initialLoadComplete = true;
|
|
1366
|
+
this._emitPbRefresh(canvases[validPage - 1]);
|
|
1367
|
+
} else if (attempts < 20) {
|
|
1368
|
+
// Not yet, retry (up to 20 attempts = 6 seconds)
|
|
1369
|
+
setTimeout(() => verifyAndEmit(attempts + 1), 300);
|
|
1370
|
+
} else {
|
|
1371
|
+
// Give up - but check registry again before emitting
|
|
1372
|
+
const finalRegistryState = registry.getState(this);
|
|
1373
|
+
if (finalRegistryState.id) {
|
|
1374
|
+
const finalRegistryPageMatch = finalRegistryState.id.match(/_(\d{2,3})\./);
|
|
1375
|
+
if (finalRegistryPageMatch) {
|
|
1376
|
+
const finalRegistryPageNum = parseInt(finalRegistryPageMatch[1], 10);
|
|
1377
|
+
if (finalRegistryPageNum !== validPage) {
|
|
1378
|
+
this._clearNavigationState();
|
|
1379
|
+
this._initialLoadComplete = true;
|
|
1380
|
+
return; // Don't emit - user has navigated
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
// Registry matches or no registry state - emit refresh
|
|
1385
|
+
this._clearNavigationState();
|
|
1386
|
+
this._initialLoadComplete = true;
|
|
1387
|
+
this._emitPbRefresh(canvases[validPage - 1]);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// Start verification after a short delay to let Tify process
|
|
1392
|
+
setTimeout(() => verifyAndEmit(), 500);
|
|
1393
|
+
} else {
|
|
1394
|
+
// Not from URL - set page and update registry
|
|
1395
|
+
this._setPage([validPage]);
|
|
1396
|
+
this._currentPage = validPage;
|
|
1397
|
+
// Update registry after a delay
|
|
1398
|
+
setTimeout(async () => {
|
|
1399
|
+
const canvas = canvases[validPage - 1];
|
|
1400
|
+
if (canvas) {
|
|
1401
|
+
await this._updateUrlFromPage(canvas);
|
|
1402
|
+
}
|
|
1403
|
+
this._initialLoadComplete = true;
|
|
1404
|
+
}, 500);
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
// Try to set initial page (will retry if canvases not ready)
|
|
1409
|
+
// If we have a URL page, use the normal setInitialPage which will load it
|
|
1410
|
+
// The setInitialPage function already checks for URL page first
|
|
1411
|
+
setInitialPage();
|
|
1412
|
+
|
|
88
1413
|
|
|
89
|
-
|
|
1414
|
+
// Process any queued navigation - $root is now available
|
|
1415
|
+
if (this._pendingNavigation) {
|
|
1416
|
+
const queuedDirection = this._pendingNavigation;
|
|
1417
|
+
this._pendingNavigation = null;
|
|
1418
|
+
this._handleNavigate(queuedDirection);
|
|
90
1419
|
}
|
|
1420
|
+
|
|
1421
|
+
// Process any queued page navigation
|
|
1422
|
+
if (this._pendingPage && this._setPage) {
|
|
1423
|
+
const queuedPages = this._pendingPage;
|
|
1424
|
+
this._pendingPage = null;
|
|
1425
|
+
this._setPage(queuedPages);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
91
1428
|
|
|
92
|
-
|
|
93
|
-
|
|
1429
|
+
/**
|
|
1430
|
+
* Set up Vue store watcher to detect page changes immediately
|
|
1431
|
+
* This provides event-driven detection for Tify's navigation (buttons, keyboard, etc.)
|
|
1432
|
+
* @param {Object} app - Tify's Vue app instance
|
|
1433
|
+
* @param {boolean} isRetry - Internal flag to prevent infinite recursion
|
|
1434
|
+
*/
|
|
1435
|
+
_setupVueStoreWatcher(app, isRetry = false) {
|
|
1436
|
+
if (!app) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Prevent duplicate setup - check if watcher is already set up
|
|
1441
|
+
// This prevents multiple intervals from being created if _setupVueStoreWatcher is called multiple times
|
|
1442
|
+
if (this._vueStoreWatcherInterval) {
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
94
1445
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1446
|
+
// Try to access Vue store - Tify uses Vue 3 with app.config.globalProperties.$store
|
|
1447
|
+
// From Tify's store.js: app.config.globalProperties.$store = new Store(options)
|
|
1448
|
+
let store = null;
|
|
1449
|
+
|
|
1450
|
+
// Try app.config.globalProperties.$store first (Vue 3 - Tify's actual structure)
|
|
1451
|
+
if (app.config && app.config.globalProperties && app.config.globalProperties.$store && app.config.globalProperties.$store.options) {
|
|
1452
|
+
store = app.config.globalProperties.$store;
|
|
1453
|
+
}
|
|
1454
|
+
// Try app.$store (Vue 2 style or fallback)
|
|
1455
|
+
else if (app.$store && app.$store.options) {
|
|
1456
|
+
store = app.$store;
|
|
1457
|
+
}
|
|
1458
|
+
// Try app.$root.$store (fallback)
|
|
1459
|
+
else if (app.$root && app.$root.$store && app.$root.$store.options) {
|
|
1460
|
+
store = app.$root.$store;
|
|
98
1461
|
}
|
|
99
1462
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1463
|
+
if (!store || !store.options) {
|
|
1464
|
+
// Store not available or doesn't have options - try to set up polling anyway
|
|
1465
|
+
// The store might become available later, so we'll poll for it
|
|
1466
|
+
console.warn('[pb-tify] Vue store not available for watcher setup, will retry with polling:', {
|
|
1467
|
+
hasApp: !!app,
|
|
1468
|
+
hasAppConfig: !!(app.config),
|
|
1469
|
+
hasGlobalProperties: !!(app.config && app.config.globalProperties),
|
|
1470
|
+
hasGlobalStore: !!(app.config && app.config.globalProperties && app.config.globalProperties.$store),
|
|
1471
|
+
hasAppStore: !!app.$store,
|
|
1472
|
+
hasAppRoot: !!app.$root,
|
|
1473
|
+
hasAppRootStore: !!(app.$root && app.$root.$store),
|
|
1474
|
+
willRetry: true
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
// Set up polling that will retry to find the store
|
|
1478
|
+
// This handles cases where the store isn't available immediately
|
|
1479
|
+
// BUT: Also set up a fallback polling mechanism that works even without the store
|
|
1480
|
+
// by directly checking Tify's viewer state
|
|
1481
|
+
let retryCount = 0;
|
|
1482
|
+
const maxRetries = 50; // Try for 5 seconds (50 * 100ms)
|
|
1483
|
+
const retryPolling = setInterval(() => {
|
|
1484
|
+
retryCount++;
|
|
1485
|
+
|
|
1486
|
+
// Check if watcher was already set up (e.g., by another call to _setupVueStoreWatcher)
|
|
1487
|
+
if (this._vueStoreWatcherInterval) {
|
|
1488
|
+
clearInterval(retryPolling);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Try to find store again
|
|
1493
|
+
let retryStore = null;
|
|
1494
|
+
if (app.config && app.config.globalProperties && app.config.globalProperties.$store && app.config.globalProperties.$store.options) {
|
|
1495
|
+
retryStore = app.config.globalProperties.$store;
|
|
1496
|
+
} else if (app.$store && app.$store.options) {
|
|
1497
|
+
retryStore = app.$store;
|
|
1498
|
+
} else if (app.$root && app.$root.$store && app.$root.$store.options) {
|
|
1499
|
+
retryStore = app.$root.$store;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (retryStore && retryStore.options) {
|
|
1503
|
+
clearInterval(retryPolling);
|
|
1504
|
+
// Recursively call to set up the watcher now that store is available
|
|
1505
|
+
// The guard at the top of _setupVueStoreWatcher will prevent duplicate setup
|
|
1506
|
+
this._setupVueStoreWatcher(app, true);
|
|
1507
|
+
} else if (retryCount >= maxRetries) {
|
|
1508
|
+
console.warn('[pb-tify] Store not found after retries, setting up fallback polling:', {
|
|
1509
|
+
retryCount,
|
|
1510
|
+
maxRetries,
|
|
1511
|
+
willUseFallback: true
|
|
1512
|
+
});
|
|
1513
|
+
clearInterval(retryPolling);
|
|
1514
|
+
// Set up fallback polling that checks Tify viewer directly
|
|
1515
|
+
// This uses the existing checkPageChange mechanism
|
|
103
1516
|
}
|
|
1517
|
+
}, 100);
|
|
1518
|
+
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Track last known pages to detect changes
|
|
1523
|
+
let lastPages = null;
|
|
1524
|
+
|
|
1525
|
+
// Helper to handle page change
|
|
1526
|
+
// This is called when Tify's Vue store detects a page change (Tify buttons/keyboard/thumbnails)
|
|
1527
|
+
// This is USER-INITIATED navigation, not programmatic, so we should always update registry
|
|
1528
|
+
const handlePageChange = async (newPage) => {
|
|
1529
|
+
// Check if another navigation source is active (not 'user')
|
|
1530
|
+
if (this._isNavigationActive() && this._navigationState.source !== 'user') {
|
|
1531
|
+
return; // Another source is navigating, skip
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// Check if we recently committed (post-navigation protection)
|
|
1535
|
+
if (this._hasRecentCommit()) {
|
|
1536
|
+
return; // Recently committed, let it settle
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// Get canvases
|
|
1540
|
+
const root = this._getRootFromApp();
|
|
1541
|
+
if (!root) {
|
|
1542
|
+
console.warn('[pb-tify] handlePageChange: no root found for page', newPage);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const canvases = this._getCanvases(root);
|
|
1547
|
+
if (newPage < 1 || newPage > canvases.length) {
|
|
1548
|
+
console.warn('[pb-tify] handlePageChange: invalid page', {
|
|
1549
|
+
newPage,
|
|
1550
|
+
totalCanvases: canvases.length,
|
|
1551
|
+
validRange: `1-${canvases.length}`
|
|
1552
|
+
});
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
104
1555
|
|
|
105
|
-
|
|
106
|
-
|
|
1556
|
+
// Always start with canvas from TEI Publisher manifest - it should have rendering property
|
|
1557
|
+
// The TEI Publisher manifest is the source of truth for rendering URLs with root/id parameters
|
|
1558
|
+
// This is the same manifest that _handleNavigate uses, which is why pb-navigation works!
|
|
1559
|
+
const canvasIndex = newPage - 1;
|
|
1560
|
+
let canvas = canvases[canvasIndex];
|
|
1561
|
+
|
|
1562
|
+
// If the canvas from the TEI Publisher manifest doesn't have rendering,
|
|
1563
|
+
// it means we're using the wrong manifest (external one). Try to get the TEI Publisher manifest.
|
|
1564
|
+
if (!canvas || !canvas.rendering || canvas.rendering.length === 0) {
|
|
1565
|
+
// The canvases array might be from the external manifest (without rendering)
|
|
1566
|
+
// Try to get the TEI Publisher manifest instead
|
|
1567
|
+
const teiPublisherRoot = this._getRootFromApp();
|
|
1568
|
+
if (teiPublisherRoot) {
|
|
1569
|
+
const teiPublisherCanvases = this._getCanvases(teiPublisherRoot);
|
|
1570
|
+
if (teiPublisherCanvases.length > 0 && newPage - 1 < teiPublisherCanvases.length) {
|
|
1571
|
+
const teiCanvas = teiPublisherCanvases[newPage - 1];
|
|
1572
|
+
// Check if this canvas has rendering (it should!)
|
|
1573
|
+
if (teiCanvas && teiCanvas.rendering && teiCanvas.rendering.length > 0) {
|
|
1574
|
+
canvas = teiCanvas;
|
|
1575
|
+
} else {
|
|
1576
|
+
// Still no rendering - try to match by label
|
|
1577
|
+
const label = canvas?.label?.none || canvas?.label?.en || canvas?.label;
|
|
1578
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
1579
|
+
if (labelStr) {
|
|
1580
|
+
for (const teiCanvas of teiPublisherCanvases) {
|
|
1581
|
+
const teiLabel = teiCanvas.label?.none || teiCanvas.label?.en || teiCanvas.label;
|
|
1582
|
+
const teiLabelStr = Array.isArray(teiLabel) ? teiLabel[0] : teiLabel;
|
|
1583
|
+
if (teiLabelStr === labelStr && teiCanvas.rendering && teiCanvas.rendering.length > 0) {
|
|
1584
|
+
canvas = teiCanvas;
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
107
1591
|
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (!canvas) {
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
108
1597
|
|
|
109
|
-
|
|
110
|
-
|
|
1598
|
+
// Always update registry when page changes, even if URL appears to match
|
|
1599
|
+
// This ensures the root is updated correctly (root can change even if id stays the same)
|
|
1600
|
+
// _updateUrlFromPage will check if commit is actually needed
|
|
1601
|
+
const renderingId = canvas?.rendering?.[0]?.['@id'] || canvas?.rendering?.[0]?.id;
|
|
1602
|
+
let canvasId = null;
|
|
1603
|
+
if (renderingId) {
|
|
1604
|
+
try {
|
|
1605
|
+
const url = new URL(renderingId, window.location.href);
|
|
1606
|
+
canvasId = url.searchParams.get('id');
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
// Ignore
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Verify canvas label matches expected page
|
|
1613
|
+
const canvasLabel = canvas?.label?.none || canvas?.label?.en || canvas?.label;
|
|
1614
|
+
const canvasLabelStr = Array.isArray(canvasLabel) ? canvasLabel[0] : canvasLabel;
|
|
1615
|
+
const expectedPageNum = String(newPage).padStart(3, '0');
|
|
1616
|
+
const canvasPageNum = canvasLabelStr ? String(canvasLabelStr).padStart(3, '0') : null;
|
|
1617
|
+
|
|
1618
|
+
// If canvas label doesn't match expected page, wait a bit and retry
|
|
1619
|
+
// This handles cases where Vue reactivity hasn't updated the canvas array yet
|
|
1620
|
+
if (canvasPageNum && canvasPageNum !== expectedPageNum) {
|
|
1621
|
+
console.warn('[pb-tify] handlePageChange: canvas label mismatch, waiting for Vue update', {
|
|
1622
|
+
expectedPageNum,
|
|
1623
|
+
canvasPageNum,
|
|
1624
|
+
newPage
|
|
111
1625
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1626
|
+
setTimeout(() => {
|
|
1627
|
+
// Retry with a fresh lookup
|
|
1628
|
+
const retryRoot = this._getRootFromApp();
|
|
1629
|
+
if (retryRoot) {
|
|
1630
|
+
const retryCanvases = this._getCanvases(retryRoot);
|
|
1631
|
+
if (newPage >= 1 && newPage <= retryCanvases.length) {
|
|
1632
|
+
const retryCanvas = retryCanvases[newPage - 1];
|
|
1633
|
+
if (retryCanvas) {
|
|
1634
|
+
this._updateUrlFromPage(retryCanvas, true);
|
|
1635
|
+
this._emitPbRefresh(retryCanvas);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
116
1638
|
}
|
|
1639
|
+
}, 50); // Wait 50ms for Vue to update
|
|
1640
|
+
return; // Exit early, retry will handle it
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// CRITICAL: Set navigation state and flags BEFORE calling _updateUrlFromPage
|
|
1644
|
+
// This prevents _handleUrlChange from resetting Tify when the URL changes
|
|
1645
|
+
// Extract the ID we're about to commit so _handleUrlChange can skip it
|
|
1646
|
+
|
|
1647
|
+
// Extract document ID from URL path (same logic as _updateUrlFromPage)
|
|
1648
|
+
let docId = null;
|
|
1649
|
+
const pathParts = window.location.pathname.split('/');
|
|
1650
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
1651
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
1652
|
+
docId = pathParts[i];
|
|
1653
|
+
break;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
let pageId = null;
|
|
1658
|
+
if (canvasId) {
|
|
1659
|
+
pageId = canvasId;
|
|
1660
|
+
} else if (canvasLabelStr && docId) {
|
|
1661
|
+
// Generate pageId from canvas label (same logic as _updateUrlFromPage)
|
|
1662
|
+
const pageNum = String(canvasLabelStr).padStart(3, '0');
|
|
1663
|
+
pageId = `${docId}_${pageNum}.jpg`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// Set navigation state for user-initiated navigation
|
|
1667
|
+
if (pageId && !this._isNavigationActive()) {
|
|
1668
|
+
this._setNavigationState('user', newPage, pageId);
|
|
1669
|
+
}
|
|
117
1670
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1671
|
+
// Update registry and emit refresh
|
|
1672
|
+
await this._updateUrlFromPage(canvas, true);
|
|
1673
|
+
|
|
1674
|
+
// Always emit pb-refresh to ensure pb-view gets notified
|
|
1675
|
+
this._emitPbRefresh(canvas);
|
|
121
1676
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1677
|
+
// Mark navigation complete for post-navigation protection
|
|
1678
|
+
setTimeout(() => {
|
|
1679
|
+
if (pageId) {
|
|
1680
|
+
this._markNavigationComplete(pageId);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Clear navigation state after a delay
|
|
1684
|
+
setTimeout(() => {
|
|
1685
|
+
this._clearNavigationState();
|
|
1686
|
+
}, 1500);
|
|
1687
|
+
}, 500);
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
// Watch for changes in store.options.pages
|
|
1691
|
+
// BEST APPROACH: Intercept both store.setPage and store.updateOptions to catch page changes immediately
|
|
1692
|
+
// This is more reliable than polling, especially in test environments
|
|
1693
|
+
|
|
1694
|
+
// Validate that store methods exist before intercepting
|
|
1695
|
+
if (typeof store.updateOptions !== 'function') {
|
|
1696
|
+
console.error('[pb-tify] store.updateOptions is not a function, cannot intercept', {
|
|
1697
|
+
storeType: typeof store,
|
|
1698
|
+
storeKeys: Object.keys(store || {}),
|
|
1699
|
+
hasUpdateOptions: 'updateOptions' in store
|
|
1700
|
+
});
|
|
1701
|
+
return; // Can't intercept if methods don't exist
|
|
1702
|
+
}
|
|
1703
|
+
if (typeof store.setPage !== 'function') {
|
|
1704
|
+
console.error('[pb-tify] store.setPage is not a function, cannot intercept', {
|
|
1705
|
+
storeType: typeof store,
|
|
1706
|
+
storeKeys: Object.keys(store || {}),
|
|
1707
|
+
hasSetPage: 'setPage' in store
|
|
1708
|
+
});
|
|
1709
|
+
return; // Can't intercept if methods don't exist
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Store original methods
|
|
1713
|
+
const originalUpdateOptions = store.updateOptions;
|
|
1714
|
+
const originalSetPage = store.setPage;
|
|
1715
|
+
let lastWatchedPages = store.options.pages ? [...store.options.pages] : null;
|
|
1716
|
+
|
|
1717
|
+
// Verify we can actually replace the methods (they might be read-only or bound)
|
|
1718
|
+
|
|
1719
|
+
// Wrap store.setPage to intercept page changes (called by goToNextPage, goToPreviousPage, etc.)
|
|
1720
|
+
store.setPage = (pageOrPages) => {
|
|
1721
|
+
// Capture old pages BEFORE calling original method
|
|
1722
|
+
const oldPages = store.options.pages ? [...store.options.pages] : null;
|
|
1723
|
+
const oldPage = oldPages && Array.isArray(oldPages)
|
|
1724
|
+
? oldPages.find(page => page > 0)
|
|
1725
|
+
: null;
|
|
1726
|
+
|
|
1727
|
+
// Call original method (this calls updateOptions internally)
|
|
1728
|
+
const result = originalSetPage.call(store, pageOrPages);
|
|
1729
|
+
|
|
1730
|
+
// The actual page change will be caught by updateOptions interception below
|
|
1731
|
+
// But we can also check here if needed
|
|
1732
|
+
return result;
|
|
1733
|
+
};
|
|
1734
|
+
|
|
1735
|
+
// Wrap store.updateOptions to intercept page changes
|
|
1736
|
+
store.updateOptions = (updatedOptions) => {
|
|
1737
|
+
// Capture old pages BEFORE calling original method (which updates the store)
|
|
1738
|
+
const oldPages = store.options.pages ? [...store.options.pages] : null;
|
|
1739
|
+
const oldPage = oldPages && Array.isArray(oldPages)
|
|
1740
|
+
? oldPages.find(page => page > 0)
|
|
1741
|
+
: null;
|
|
1742
|
+
|
|
1743
|
+
// Call original method (this updates the store)
|
|
1744
|
+
const result = originalUpdateOptions.call(store, updatedOptions);
|
|
1745
|
+
|
|
1746
|
+
// Check if pages changed - either from updatedOptions OR from store.options.pages after update
|
|
1747
|
+
// Tify might update pages directly on the reactive store, or call updateOptions multiple times
|
|
1748
|
+
// Use a small delay to ensure Vue reactivity has updated store.options.pages
|
|
1749
|
+
// This is especially important for the first click in test environments
|
|
1750
|
+
setTimeout(() => {
|
|
1751
|
+
const newPagesFromOptions = updatedOptions && updatedOptions.pages ? updatedOptions.pages : null;
|
|
1752
|
+
const newPagesFromStore = store.options.pages ? [...store.options.pages] : null;
|
|
1753
|
+
const newPages = newPagesFromOptions || newPagesFromStore;
|
|
1754
|
+
|
|
1755
|
+
if (newPages) {
|
|
1756
|
+
const newPage = Array.isArray(newPages) ? newPages.find(page => page > 0) : null;
|
|
1757
|
+
const lastKnownPage = lastWatchedPages && Array.isArray(lastWatchedPages)
|
|
1758
|
+
? lastWatchedPages.find(page => page > 0)
|
|
1759
|
+
: null;
|
|
1760
|
+
|
|
1761
|
+
// Only handle if page actually changed from the last known state
|
|
1762
|
+
// Use lastWatchedPages (tracked across all calls) rather than oldPage (just this call)
|
|
1763
|
+
if (newPage && newPage !== lastKnownPage && newPage > 0) {
|
|
1764
|
+
// Update last watched pages immediately
|
|
1765
|
+
lastWatchedPages = Array.isArray(newPages) ? [...newPages] : null;
|
|
1766
|
+
|
|
1767
|
+
// CRITICAL: Set navigation state to prevent checkPageChange from interfering while we wait for Tify
|
|
1768
|
+
// This prevents checkPageChange from committing page 1 while we're waiting for page 2
|
|
1769
|
+
// Use navigation state instead of _isCommitting to avoid conflicts with handlePageChange
|
|
1770
|
+
const root = this._getRootFromApp();
|
|
1771
|
+
const canvases = this._getCanvases(root);
|
|
1772
|
+
if (canvases && canvases[newPage - 1]) {
|
|
1773
|
+
const canvas = canvases[newPage - 1];
|
|
1774
|
+
const canvasLabel = canvas.label?.none || canvas.label?.en || canvas.label;
|
|
1775
|
+
const labelStr = Array.isArray(canvasLabel) ? canvasLabel[0] : canvasLabel;
|
|
1776
|
+
if (labelStr) {
|
|
1777
|
+
const pathParts = window.location.pathname.split('/');
|
|
1778
|
+
let docId = null;
|
|
1779
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
1780
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
1781
|
+
docId = pathParts[i];
|
|
1782
|
+
break;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (docId) {
|
|
1786
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
1787
|
+
const pageId = `${docId}_${pageNum}.jpg`;
|
|
1788
|
+
this._setNavigationState('user', newPage, pageId);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Wait for Tify's viewer to actually update to the new page before calling handlePageChange
|
|
1794
|
+
// This ensures the canvas lookup gets the correct canvas
|
|
1795
|
+
const waitForTifyUpdate = (attempts = 0) => {
|
|
1796
|
+
const maxAttempts = 20; // Try for up to 200ms (20 * 10ms)
|
|
1797
|
+
|
|
1798
|
+
if (!this._tify || !this._tify.viewer) {
|
|
1799
|
+
// Viewer not ready yet, wait a bit more
|
|
1800
|
+
if (attempts < maxAttempts) {
|
|
1801
|
+
setTimeout(() => waitForTifyUpdate(attempts + 1), 10);
|
|
1802
|
+
} else {
|
|
1803
|
+
// Give up and call handlePageChange anyway
|
|
1804
|
+
console.warn('[pb-tify] Viewer not ready after waiting, calling handlePageChange anyway');
|
|
1805
|
+
// handlePageChange will manage navigation state and _isCommitting
|
|
1806
|
+
handlePageChange(newPage);
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Check Tify's internal page state
|
|
1812
|
+
let tifyCurrentPage = null;
|
|
1813
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
1814
|
+
try {
|
|
1815
|
+
const page = this._tify.viewer.currentPage();
|
|
1816
|
+
if (typeof page === 'number') {
|
|
1817
|
+
tifyCurrentPage = page + 1; // Convert to 1-indexed
|
|
126
1818
|
}
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
// Ignore
|
|
1821
|
+
}
|
|
1822
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
1823
|
+
tifyCurrentPage = this._tify.viewer._sequenceIndex + 1;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (tifyCurrentPage === newPage) {
|
|
1827
|
+
// Tify has updated to the correct page, safe to call handlePageChange
|
|
1828
|
+
// handlePageChange will manage navigation state and _isCommitting
|
|
1829
|
+
handlePageChange(newPage);
|
|
1830
|
+
} else if (attempts < maxAttempts) {
|
|
1831
|
+
// Tify hasn't updated yet, wait a bit more
|
|
1832
|
+
setTimeout(() => waitForTifyUpdate(attempts + 1), 10);
|
|
1833
|
+
} else {
|
|
1834
|
+
// Give up after max attempts - call handlePageChange anyway
|
|
1835
|
+
// handlePageChange will manage navigation state and _isCommitting
|
|
1836
|
+
handlePageChange(newPage);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
// Start waiting for Tify to update
|
|
1841
|
+
waitForTifyUpdate();
|
|
1842
|
+
} else {
|
|
1843
|
+
// Update last watched pages even if page didn't change (for consistency)
|
|
1844
|
+
lastWatchedPages = Array.isArray(newPages) ? [...newPages] : null;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}, 0); // Use setTimeout with 0ms delay to ensure Vue reactivity has updated store.options.pages
|
|
1848
|
+
|
|
1849
|
+
return result;
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// FALLBACK: Also use polling as backup (in case updateOptions isn't called directly)
|
|
1853
|
+
// This is more reliable than trying to set up a Vue watcher from outside the component tree
|
|
1854
|
+
|
|
1855
|
+
// Poll the reactive store directly (more reliable than trying to set up Vue watcher)
|
|
1856
|
+
const watchStorePages = () => {
|
|
1857
|
+
if (!store || !store.options || !store.options.pages) {
|
|
1858
|
+
// Log when store is not accessible (but throttle to avoid spam)
|
|
1859
|
+
if (Date.now() % 1000 < 100) { // Log roughly once per second when store is inaccessible
|
|
1860
|
+
console.warn('[pb-tify] Store not accessible in polling:', {
|
|
1861
|
+
hasStore: !!store,
|
|
1862
|
+
hasOptions: !!(store && store.options),
|
|
1863
|
+
hasPages: !!(store && store.options && store.options.pages),
|
|
1864
|
+
timestamp: Date.now()
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
127
1869
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
1870
|
+
const currentPages = store.options.pages;
|
|
1871
|
+
const currentPage = Array.isArray(currentPages) ? currentPages.find(page => page > 0) : null;
|
|
1872
|
+
const lastPage = lastWatchedPages && Array.isArray(lastWatchedPages)
|
|
1873
|
+
? lastWatchedPages.find(page => page > 0)
|
|
1874
|
+
: null;
|
|
134
1875
|
|
|
135
|
-
|
|
136
|
-
|
|
1876
|
+
if (currentPage && currentPage !== lastPage) {
|
|
1877
|
+
handlePageChange(currentPage);
|
|
1878
|
+
lastWatchedPages = currentPages ? [...currentPages] : null;
|
|
1879
|
+
} else if (currentPages && !lastWatchedPages) {
|
|
1880
|
+
lastWatchedPages = [...currentPages];
|
|
1881
|
+
} else if (currentPages) {
|
|
1882
|
+
// Pages haven't changed, but store is accessible - polling is working
|
|
1883
|
+
// Don't log this - it's too verbose and creates console spam
|
|
1884
|
+
// The polling is working correctly, no need to log every 5 seconds
|
|
1885
|
+
lastWatchedPages = [...currentPages];
|
|
1886
|
+
} else {
|
|
1887
|
+
// Log if store becomes inaccessible
|
|
1888
|
+
if (lastWatchedPages) {
|
|
1889
|
+
console.warn('[pb-tify] Store pages became inaccessible (polling):', {
|
|
1890
|
+
hadPages: !!lastWatchedPages,
|
|
1891
|
+
storeAccessible: !!store,
|
|
1892
|
+
hasOptions: !!(store && store.options),
|
|
1893
|
+
hasPages: !!(store && store.options && store.options.pages)
|
|
1894
|
+
});
|
|
1895
|
+
lastWatchedPages = null;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
|
|
1900
|
+
// Poll the reactive store as a fallback (in case interception misses something)
|
|
1901
|
+
// Use a reasonable interval - 200ms is fast enough for good UX without excessive CPU usage
|
|
1902
|
+
// The interception of store.updateOptions should catch most changes immediately
|
|
1903
|
+
this._vueStoreWatcherInterval = setInterval(watchStorePages, 200);
|
|
1904
|
+
|
|
1905
|
+
// Also try app.$watch as a backup (might work in some Vue setups)
|
|
1906
|
+
if (typeof app.$watch === 'function') {
|
|
1907
|
+
try {
|
|
1908
|
+
this._vueStoreWatcher = app.$watch(
|
|
1909
|
+
() => store.options.pages,
|
|
1910
|
+
(newPages, oldPages) => {
|
|
1911
|
+
|
|
1912
|
+
// Only handle if pages actually changed
|
|
1913
|
+
if (!oldPages || !newPages || !Array.isArray(newPages) || newPages.length === 0) {
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Get first valid page (page > 0)
|
|
1918
|
+
const newPage = newPages.find(page => page > 0);
|
|
1919
|
+
const oldPage = oldPages && oldPages.length > 0 ? oldPages.find(page => page > 0) : null;
|
|
1920
|
+
|
|
1921
|
+
// Check if page actually changed
|
|
1922
|
+
if (newPage && newPage !== oldPage && newPage > 0) {
|
|
1923
|
+
handlePageChange(newPage);
|
|
1924
|
+
}
|
|
1925
|
+
},
|
|
1926
|
+
{ deep: true, immediate: false }
|
|
1927
|
+
);
|
|
1928
|
+
} catch (e) {
|
|
1929
|
+
console.warn('[pb-tify] app.$watch failed, using polling only:', e);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// Fallback: Use Vue 3 watch API directly if available
|
|
1933
|
+
// Try to import watch from Vue if we can access it
|
|
1934
|
+
else if (typeof window !== 'undefined' && window.Vue && window.Vue.watch) {
|
|
1935
|
+
// Vue 3 watch function available globally
|
|
1936
|
+
this._vueStoreWatcher = window.Vue.watch(
|
|
1937
|
+
() => store.options.pages,
|
|
1938
|
+
(newPages, oldPages) => {
|
|
1939
|
+
|
|
1940
|
+
if (!oldPages || !newPages || !Array.isArray(newPages) || newPages.length === 0) {
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const newPage = newPages.find(page => page > 0);
|
|
1945
|
+
const oldPage = oldPages && oldPages.length > 0 ? oldPages.find(page => page > 0) : null;
|
|
1946
|
+
|
|
1947
|
+
if (newPage && newPage !== oldPage && newPage > 0) {
|
|
1948
|
+
handlePageChange(newPage);
|
|
1949
|
+
}
|
|
1950
|
+
},
|
|
1951
|
+
{ deep: true, immediate: false }
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
// If neither works, fall back to polling (already set up in checkPageChange)
|
|
1955
|
+
else {
|
|
1956
|
+
console.warn('[pb-tify] Vue store watcher could not be set up - falling back to polling');
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
_handleManifestError(error) {
|
|
1961
|
+
// Clear any existing error message
|
|
1962
|
+
this._clearError();
|
|
137
1963
|
|
|
138
|
-
|
|
1964
|
+
// Create error message element
|
|
1965
|
+
const errorDiv = document.createElement('div');
|
|
1966
|
+
errorDiv.className = 'pb-tify-error';
|
|
1967
|
+
errorDiv.style.cssText = `
|
|
1968
|
+
display: flex;
|
|
1969
|
+
align-items: center;
|
|
1970
|
+
justify-content: center;
|
|
1971
|
+
height: 100%;
|
|
1972
|
+
width: 100%;
|
|
1973
|
+
background-color: #f8f9fa;
|
|
1974
|
+
border: 1px solid #dee2e6;
|
|
1975
|
+
border-radius: 4px;
|
|
1976
|
+
color: #6c757d;
|
|
1977
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1978
|
+
font-size: 14px;
|
|
1979
|
+
text-align: center;
|
|
1980
|
+
padding: 20px;
|
|
1981
|
+
`;
|
|
1982
|
+
|
|
1983
|
+
// Determine error message based on error type
|
|
1984
|
+
let errorMessage = 'Failed to load IIIF manifest';
|
|
1985
|
+
|
|
1986
|
+
// Check error message, status, and other properties
|
|
1987
|
+
const errorText = error.message || error.toString() || '';
|
|
1988
|
+
const status = error.status || error.statusCode;
|
|
1989
|
+
|
|
1990
|
+
if (status === 404 || errorText.includes('404') || errorText.includes('Not Found')) {
|
|
1991
|
+
errorMessage = 'IIIF manifest not found';
|
|
1992
|
+
} else if (status === 403 || errorText.includes('403') || errorText.includes('Forbidden')) {
|
|
1993
|
+
errorMessage = 'Access denied to IIIF manifest';
|
|
1994
|
+
} else if (
|
|
1995
|
+
errorText.includes('NetworkError') ||
|
|
1996
|
+
errorText.includes('Failed to fetch') ||
|
|
1997
|
+
errorText.includes('network')
|
|
1998
|
+
) {
|
|
1999
|
+
errorMessage = 'Network error loading IIIF manifest';
|
|
2000
|
+
} else if (
|
|
2001
|
+
errorText.includes('Invalid JSON') ||
|
|
2002
|
+
errorText.includes('SyntaxError') ||
|
|
2003
|
+
errorText.includes('parse') ||
|
|
2004
|
+
errorText.includes('Unexpected token') ||
|
|
2005
|
+
errorText.includes('JSON') ||
|
|
2006
|
+
errorText.includes('$meta') ||
|
|
2007
|
+
errorText.includes('manifest')
|
|
2008
|
+
) {
|
|
2009
|
+
errorMessage = 'Invalid IIIF manifest format';
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
errorDiv.textContent = errorMessage;
|
|
2013
|
+
|
|
2014
|
+
// Add error element to container
|
|
2015
|
+
if (this._container) {
|
|
2016
|
+
this._container.appendChild(errorDiv);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Emit error event for parent components to handle
|
|
2020
|
+
this.emitTo('pb-tify-error', {
|
|
2021
|
+
error: error.message || 'Unknown error',
|
|
2022
|
+
manifest: this.manifest,
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
_clearError() {
|
|
2027
|
+
if (this._container) {
|
|
2028
|
+
const existingError = this._container.querySelector('.pb-tify-error');
|
|
2029
|
+
if (existingError) {
|
|
2030
|
+
existingError.remove();
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Set up watcher for Tify's error store
|
|
2037
|
+
* This allows us to detect errors that Tify handles internally
|
|
2038
|
+
* and display them using our error handling system
|
|
2039
|
+
*/
|
|
2040
|
+
_setupTifyErrorWatcher() {
|
|
2041
|
+
if (!this._tify || !this._tify.app) {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// Try to access Tify's store
|
|
2046
|
+
const store = this._tify.app.config?.globalProperties?.$store ||
|
|
2047
|
+
this._tify.app.$store ||
|
|
2048
|
+
(this._tify.app.$root && this._tify.app.$root.$store);
|
|
2049
|
+
|
|
2050
|
+
if (!store || !store.errors) {
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// Watch for errors in Tify's store
|
|
2055
|
+
// Use polling since we can't easily set up Vue watchers from outside
|
|
2056
|
+
let lastErrorCount = store.errors.size;
|
|
2057
|
+
this._tifyErrorWatcherInterval = setInterval(() => {
|
|
2058
|
+
if (!this._tify || !store || !store.errors) {
|
|
2059
|
+
clearInterval(this._tifyErrorWatcherInterval);
|
|
2060
|
+
this._tifyErrorWatcherInterval = null;
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
const currentErrorCount = store.errors.size;
|
|
2065
|
+
if (currentErrorCount > lastErrorCount) {
|
|
2066
|
+
// New errors were added
|
|
2067
|
+
const newErrors = Array.from(store.errors).slice(lastErrorCount);
|
|
2068
|
+
for (const errorMessage of newErrors) {
|
|
2069
|
+
// Create error object from Tify's error message
|
|
2070
|
+
const error = new Error(errorMessage);
|
|
2071
|
+
// Try to extract status code from error message
|
|
2072
|
+
if (errorMessage.includes('404') || errorMessage.includes('Not Found')) {
|
|
2073
|
+
error.status = 404;
|
|
2074
|
+
} else if (errorMessage.includes('403') || errorMessage.includes('Forbidden')) {
|
|
2075
|
+
error.status = 403;
|
|
2076
|
+
}
|
|
2077
|
+
this._handleManifestError(error);
|
|
2078
|
+
}
|
|
2079
|
+
lastErrorCount = currentErrorCount;
|
|
2080
|
+
} else if (currentErrorCount < lastErrorCount) {
|
|
2081
|
+
// Errors were cleared
|
|
2082
|
+
lastErrorCount = currentErrorCount;
|
|
2083
|
+
this._clearError();
|
|
2084
|
+
}
|
|
2085
|
+
}, 200); // Check every 200ms
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Navigation State Management
|
|
2091
|
+
* ==========================
|
|
2092
|
+
*
|
|
2093
|
+
* pb-tify uses a single _navigationState object to track all navigation activities.
|
|
2094
|
+
* This replaces the previous system of 7 separate boolean flags.
|
|
2095
|
+
*
|
|
2096
|
+
* _navigationState object structure:
|
|
2097
|
+
* {
|
|
2098
|
+
* source: 'user' | 'thumbnail' | 'programmatic' | 'url',
|
|
2099
|
+
* targetPage: number,
|
|
2100
|
+
* targetId: string,
|
|
2101
|
+
* timestamp: number,
|
|
2102
|
+
* isActive: boolean,
|
|
2103
|
+
* recentlyCommittedId: string | null, // ID of recently committed page
|
|
2104
|
+
* completedAt: number | null // When navigation completed
|
|
2105
|
+
* }
|
|
2106
|
+
*/
|
|
2107
|
+
_setNavigationState(source, targetPage, targetId) {
|
|
2108
|
+
this._navigationState = {
|
|
2109
|
+
source, // 'user', 'thumbnail', 'programmatic', 'url'
|
|
2110
|
+
targetPage,
|
|
2111
|
+
targetId,
|
|
2112
|
+
timestamp: Date.now(),
|
|
2113
|
+
isActive: true,
|
|
2114
|
+
recentlyCommittedId: null,
|
|
2115
|
+
completedAt: null
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* Mark navigation as complete, storing the committed ID for post-navigation protection
|
|
2121
|
+
* @param {string} committedId - The ID that was committed
|
|
2122
|
+
*/
|
|
2123
|
+
_markNavigationComplete(committedId) {
|
|
2124
|
+
if (this._navigationState) {
|
|
2125
|
+
this._navigationState.isActive = false;
|
|
2126
|
+
this._navigationState.recentlyCommittedId = committedId;
|
|
2127
|
+
this._navigationState.completedAt = Date.now();
|
|
139
2128
|
}
|
|
2129
|
+
}
|
|
140
2130
|
|
|
141
|
-
|
|
142
|
-
|
|
2131
|
+
_clearNavigationState() {
|
|
2132
|
+
this._navigationState = null;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
_isNavigationActive(source = null) {
|
|
2136
|
+
if (!this._navigationState || !this._navigationState.isActive) {
|
|
2137
|
+
return false;
|
|
2138
|
+
}
|
|
2139
|
+
// If source is specified, only return true if that specific source is active
|
|
2140
|
+
if (source) {
|
|
2141
|
+
return this._navigationState.source === source;
|
|
2142
|
+
}
|
|
2143
|
+
// If no source specified, return true if any navigation is active
|
|
2144
|
+
return true;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
_matchesNavigationTarget(id) {
|
|
2148
|
+
if (!this._navigationState || !this._navigationState.isActive) {
|
|
2149
|
+
return false;
|
|
2150
|
+
}
|
|
2151
|
+
return this._navigationState.targetId === id;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Check if an ID was recently committed (within maxAge ms)
|
|
2156
|
+
* Used for post-navigation protection to prevent bounce-back
|
|
2157
|
+
* @param {string} id - The ID to check
|
|
2158
|
+
* @param {number} maxAge - Maximum age in ms (default 3000ms)
|
|
2159
|
+
* @returns {boolean}
|
|
2160
|
+
*/
|
|
2161
|
+
_isRecentlyCommitted(id, maxAge = 3000) {
|
|
2162
|
+
if (!this._navigationState || !this._navigationState.recentlyCommittedId) {
|
|
2163
|
+
return false;
|
|
2164
|
+
}
|
|
2165
|
+
if (this._navigationState.recentlyCommittedId !== id) {
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
if (!this._navigationState.completedAt) {
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
return Date.now() - this._navigationState.completedAt < maxAge;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* Check if we recently committed any page (within maxAge ms)
|
|
2176
|
+
* @param {number} maxAge - Maximum age in ms (default 3000ms)
|
|
2177
|
+
* @returns {boolean}
|
|
2178
|
+
*/
|
|
2179
|
+
_hasRecentCommit(maxAge = 3000) {
|
|
2180
|
+
if (!this._navigationState || !this._navigationState.recentlyCommittedId) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
if (!this._navigationState.completedAt) {
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
return Date.now() - this._navigationState.completedAt < maxAge;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
/**
|
|
2190
|
+
* Get the recently committed ID (if any)
|
|
2191
|
+
* @returns {string|null}
|
|
2192
|
+
*/
|
|
2193
|
+
_getRecentlyCommittedId() {
|
|
2194
|
+
if (!this._navigationState) {
|
|
2195
|
+
return null;
|
|
2196
|
+
}
|
|
2197
|
+
return this._navigationState.recentlyCommittedId;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Update URL from current canvas/page.
|
|
2202
|
+
* Extracts root/id from canvas rendering URL and commits to registry.
|
|
2203
|
+
* Also updates hash fragment based on canvas label (e.g., #A-N-38_005.jpg).
|
|
2204
|
+
* @param {Object} canvas - The canvas object
|
|
2205
|
+
* @param {boolean} force - Force update even if updating from registry
|
|
2206
|
+
* @returns {Promise<void>} - Resolves when URL update is complete
|
|
2207
|
+
*/
|
|
2208
|
+
async _updateUrlFromPage(canvas, force = false) {
|
|
2209
|
+
|
|
2210
|
+
// CRITICAL: If we just committed a page change and we're trying to commit a different page, skip to prevent bounce-back
|
|
2211
|
+
// This prevents checkPageChange or _handleUrlChange from overwriting what we just committed
|
|
2212
|
+
// Check both _lastCommittedId AND current registry state to prevent overwriting recent commits
|
|
2213
|
+
const registryState = registry.getState(this);
|
|
2214
|
+
|
|
2215
|
+
// Generate the pageId for this canvas to compare
|
|
2216
|
+
let canvasPageId = null;
|
|
2217
|
+
const canvasLabel = canvas?.label?.none || canvas?.label?.en || canvas?.label;
|
|
2218
|
+
const canvasLabelStr = Array.isArray(canvasLabel) ? canvasLabel[0] : canvasLabel;
|
|
2219
|
+
if (canvasLabelStr) {
|
|
2220
|
+
const pathParts = window.location.pathname.split('/');
|
|
2221
|
+
let docId = null;
|
|
2222
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
2223
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
2224
|
+
docId = pathParts[i];
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (docId) {
|
|
2229
|
+
const pageNum = String(canvasLabelStr).padStart(3, '0');
|
|
2230
|
+
canvasPageId = `${docId}_${pageNum}.jpg`;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
// Also try to extract from rendering URL
|
|
2234
|
+
if (!canvasPageId && canvas?.rendering && canvas.rendering.length > 0) {
|
|
2235
|
+
const renderingId = canvas.rendering[0]['@id'] || canvas.rendering[0].id;
|
|
2236
|
+
if (renderingId) {
|
|
2237
|
+
try {
|
|
2238
|
+
const url = new URL(renderingId, window.location.href);
|
|
2239
|
+
canvasPageId = url.searchParams.get('id');
|
|
2240
|
+
} catch (e) {
|
|
2241
|
+
// Ignore
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// CRITICAL: Prevent initial load handlers from overwriting user navigation
|
|
2247
|
+
// If initial load is not complete AND registry already has a different page, check if we should skip
|
|
2248
|
+
if (!this._initialLoadComplete && registryState.id) {
|
|
2249
|
+
// Try to extract page numbers from both sources
|
|
2250
|
+
const registryPageNum = registryState.id.match(/_(\d{2,3})\./);
|
|
2251
|
+
let canvasPageNum = null;
|
|
2252
|
+
|
|
2253
|
+
// Try to get page number from canvasPageId first
|
|
2254
|
+
if (canvasPageId) {
|
|
2255
|
+
const match = canvasPageId.match(/_(\d{2,3})\./);
|
|
2256
|
+
if (match) canvasPageNum = match;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// If canvasPageId extraction failed, try to extract from canvas label directly
|
|
2260
|
+
if (!canvasPageNum && canvasLabelStr) {
|
|
2261
|
+
const labelMatch = String(canvasLabelStr).match(/(\d{2,3})/);
|
|
2262
|
+
if (labelMatch) {
|
|
2263
|
+
canvasPageNum = [null, labelMatch[1]]; // Match format: [fullMatch, pageNum]
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// If we have both page numbers and registry has a newer page, skip and mark as complete
|
|
2268
|
+
if (registryPageNum && canvasPageNum &&
|
|
2269
|
+
parseInt(canvasPageNum[1], 10) < parseInt(registryPageNum[1], 10)) {
|
|
2270
|
+
this._initialLoadComplete = true;
|
|
2271
|
+
return; // Skip entirely - don't even update hash
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// Simplified state check: if another source is navigating to a different target, skip
|
|
2276
|
+
// BUT: If force=true (from handlePageChange), always proceed - it's an intentional navigation
|
|
2277
|
+
if (!force && this._navigationState && this._navigationState.isActive && canvasPageId) {
|
|
2278
|
+
// If navigation is active and this doesn't match the target, skip
|
|
2279
|
+
// Exception: if source is 'user', allow it (user navigation should proceed)
|
|
2280
|
+
if (!this._matchesNavigationTarget(canvasPageId) &&
|
|
2281
|
+
this._navigationState.source !== 'user') {
|
|
2282
|
+
return; // Another source is navigating to a different target
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
// Skip if we're not in forced mode and another navigation is active (not ours)
|
|
2287
|
+
// This prevents checkPageChange from overwriting intentional navigation
|
|
2288
|
+
if (!force && this._isNavigationActive() && !this._matchesNavigationTarget(canvasPageId)) {
|
|
2289
|
+
return; // Another navigation is active, skip
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Skip if we recently committed and this isn't a forced update
|
|
2293
|
+
if (!force && this._hasRecentCommit()) {
|
|
2294
|
+
const recentId = this._getRecentlyCommittedId();
|
|
2295
|
+
// Only skip if we're trying to commit something different from what we recently committed
|
|
2296
|
+
if (recentId && canvasPageId && canvasPageId !== recentId) {
|
|
2297
|
+
// Check if we're trying to commit an older page (wouldDowngrade check)
|
|
2298
|
+
const recentPageNum = recentId.match(/_(\d{2,3})\./);
|
|
2299
|
+
const canvasPageNum = canvasPageId.match(/_(\d{2,3})\./);
|
|
2300
|
+
if (recentPageNum && canvasPageNum &&
|
|
2301
|
+
parseInt(canvasPageNum[1], 10) < parseInt(recentPageNum[1], 10)) {
|
|
2302
|
+
return; // Skip - would downgrade to older page
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// If force=true (from handlePageChange) and state already matches, emit pb-refresh but skip commit
|
|
2308
|
+
if (force && this._isNavigationActive() && canvasPageId && registryState.id === canvasPageId) {
|
|
2309
|
+
setTimeout(() => {
|
|
2310
|
+
this._emitPbRefresh(canvas);
|
|
2311
|
+
}, 50);
|
|
2312
|
+
return; // Skip commit but pb-refresh was emitted
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
if (!canvas) {
|
|
2316
|
+
console.warn('[pb-tify] _updateUrlFromPage: skipping - no canvas provided');
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// If canvas doesn't have rendering property, try to get it from manifest
|
|
2321
|
+
// The manifest canvas should always have rendering with root/id parameters
|
|
2322
|
+
// root should be an eXist-db node ID like "3.5.6.1" extracted from rendering URL
|
|
2323
|
+
if (!canvas.rendering || canvas.rendering.length === 0) {
|
|
2324
|
+
const root = this._getRootFromApp();
|
|
2325
|
+
if (root) {
|
|
2326
|
+
const canvases = this._getCanvases(root);
|
|
2327
|
+
// Try to find matching canvas in manifest by @id first (most reliable)
|
|
2328
|
+
if (canvas['@id']) {
|
|
2329
|
+
for (const manifestCanvas of canvases) {
|
|
2330
|
+
if (manifestCanvas['@id'] === canvas['@id']) {
|
|
2331
|
+
// Found matching canvas in manifest - use it for rendering
|
|
2332
|
+
canvas = manifestCanvas;
|
|
2333
|
+
break;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
// Fallback: try matching by label if @id didn't work
|
|
2338
|
+
if ((!canvas.rendering || canvas.rendering.length === 0) && canvas.label) {
|
|
2339
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
2340
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
2341
|
+
if (labelStr) {
|
|
2342
|
+
for (const manifestCanvas of canvases) {
|
|
2343
|
+
const manifestLabel = manifestCanvas.label?.none || manifestCanvas.label?.en || manifestCanvas.label;
|
|
2344
|
+
const manifestLabelStr = Array.isArray(manifestLabel) ? manifestLabel[0] : manifestLabel;
|
|
2345
|
+
if (manifestLabelStr === labelStr) {
|
|
2346
|
+
// Found matching canvas in manifest - use it for rendering
|
|
2347
|
+
canvas = manifestCanvas;
|
|
2348
|
+
break;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// If still no rendering, try to get root from page number → root lookup map
|
|
2356
|
+
// This map is built from the TEI Publisher manifest which includes rendering URLs with root
|
|
2357
|
+
if ((!canvas.rendering || canvas.rendering.length === 0) && canvas.label) {
|
|
2358
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
2359
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
2360
|
+
if (labelStr) {
|
|
2361
|
+
// Ensure we have the page-to-root map
|
|
2362
|
+
await this._ensurePageToRootMap();
|
|
2363
|
+
|
|
2364
|
+
// Look up root by page number (e.g., "011")
|
|
2365
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
2366
|
+
const root = this._pageToRootMap && this._pageToRootMap[pageNum];
|
|
2367
|
+
|
|
2368
|
+
if (root) {
|
|
2369
|
+
// Found root! Construct rendering URL and add it to canvas
|
|
2370
|
+
const pathParts = window.location.pathname.split('/');
|
|
2371
|
+
let docPath = null;
|
|
2372
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
2373
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
2374
|
+
docPath = pathParts[i];
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
if (docPath) {
|
|
2380
|
+
const endpoint = this.getEndpoint();
|
|
2381
|
+
const renderingUrl = `${endpoint}/api/parts/${encodeURIComponent(docPath)}/html?root=${encodeURIComponent(root)}`;
|
|
2382
|
+
|
|
2383
|
+
if (!canvas.rendering) {
|
|
2384
|
+
canvas.rendering = [];
|
|
2385
|
+
}
|
|
2386
|
+
canvas.rendering.push({
|
|
2387
|
+
'@id': renderingUrl,
|
|
2388
|
+
id: renderingUrl,
|
|
2389
|
+
type: 'Text',
|
|
2390
|
+
format: 'text/html'
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
} else {
|
|
2394
|
+
console.warn('[pb-tify] _updateUrlFromPage: root not found in lookup map for page', { pageNum, labelStr });
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
// Extract document ID from URL path (e.g., /exist/apps/mf-app/A-N-38 -> A-N-38)
|
|
2401
|
+
// Or from manifest URL if available
|
|
2402
|
+
let docId = null;
|
|
2403
|
+
const pathParts = window.location.pathname.split('/');
|
|
2404
|
+
// Look for the document ID in the path (usually the last non-empty part before query)
|
|
2405
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
2406
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
2407
|
+
docId = pathParts[i];
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
// If no docId found in path, try to extract from manifest URL
|
|
2413
|
+
if (!docId && this.manifest) {
|
|
2414
|
+
try {
|
|
2415
|
+
const manifestUrl = new URL(this.manifest, window.location.origin);
|
|
2416
|
+
const manifestPath = manifestUrl.pathname.split('/');
|
|
2417
|
+
docId = manifestPath[manifestPath.length - 1];
|
|
2418
|
+
} catch (e) {
|
|
2419
|
+
// Ignore
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
// Get canvas label for generating id and hash
|
|
2424
|
+
let pageId = null; // Format: A-N-38_002.jpg
|
|
2425
|
+
let hashFragment = null; // Format: #A-N-38_002.jpg
|
|
2426
|
+
|
|
2427
|
+
if (canvas.label && docId) {
|
|
2428
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
2429
|
+
let labelStr = null;
|
|
2430
|
+
|
|
2431
|
+
if (Array.isArray(label)) {
|
|
2432
|
+
labelStr = label[0];
|
|
2433
|
+
} else if (typeof label === 'string') {
|
|
2434
|
+
labelStr = label;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
if (labelStr) {
|
|
2438
|
+
// Pad label to 3 digits (e.g., "2" -> "002", "10" -> "010")
|
|
2439
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
2440
|
+
// Format: DOCUMENT_ID_PAGENUM.jpg (e.g., A-N-38_002.jpg)
|
|
2441
|
+
pageId = `${docId}_${pageNum}.jpg`;
|
|
2442
|
+
hashFragment = `#${pageId}`;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// Try to extract root/id from rendering URL (may override pageId)
|
|
2447
|
+
// If no rendering property, we may have just fetched it above - check again
|
|
2448
|
+
let root = null;
|
|
2449
|
+
let id = null;
|
|
2450
|
+
const { rendering } = canvas;
|
|
2451
|
+
if (rendering && rendering.length > 0) {
|
|
2452
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
2453
|
+
if (renderingId) {
|
|
2454
|
+
try {
|
|
2455
|
+
// Handle both absolute and relative URLs
|
|
2456
|
+
let url;
|
|
2457
|
+
if (renderingId.startsWith('http://') || renderingId.startsWith('https://')) {
|
|
2458
|
+
url = new URL(renderingId);
|
|
2459
|
+
} else {
|
|
2460
|
+
// Relative URL - convert to absolute using current origin
|
|
2461
|
+
url = new URL(renderingId, window.location.origin);
|
|
2462
|
+
}
|
|
2463
|
+
root = url.searchParams.get('root');
|
|
2464
|
+
id = url.searchParams.get('id');
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
console.warn('[pb-tify] _updateUrlFromPage: Error parsing rendering URL:', renderingId, error);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// Use id from rendering URL if available, otherwise use generated pageId
|
|
2472
|
+
const finalId = id || pageId;
|
|
2473
|
+
|
|
2474
|
+
// Check if we're already on this page (prevent duplicate commits)
|
|
2475
|
+
const currentState = registry.getState(this);
|
|
2476
|
+
const idMatches = finalId && currentState.id === finalId;
|
|
2477
|
+
const rootMatches = root && currentState.root === root;
|
|
2478
|
+
const alreadyMatches = idMatches && (rootMatches || !root); // Match if id matches and (root matches or no root provided)
|
|
2479
|
+
|
|
2480
|
+
// Check if root has changed (critical for transcription sync)
|
|
2481
|
+
const rootChanged = root && currentState.root !== root;
|
|
2482
|
+
|
|
2483
|
+
// Check if URL is missing id parameter in query string (needed for shareable URLs)
|
|
2484
|
+
// Even if state matches, we need to ensure id is in query params AND hash
|
|
2485
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2486
|
+
const urlHasIdParam = urlParams.has('id') && urlParams.get('id') === finalId;
|
|
2487
|
+
const needsIdInQuery = finalId && !urlHasIdParam;
|
|
2488
|
+
|
|
2489
|
+
// Commit if different from current state OR if URL is missing id parameter OR if root changed
|
|
2490
|
+
// Root changes are critical for transcription sync, so always commit when root changes
|
|
2491
|
+
// CRITICAL: If force=true (from handlePageChange), always commit even if state appears to match
|
|
2492
|
+
// This ensures user-initiated navigation always updates the URL
|
|
2493
|
+
// Also commit if navigation state is active and matches target (intentional navigation)
|
|
2494
|
+
const isNavigationActive = this._navigationState && this._navigationState.isActive;
|
|
2495
|
+
const matchesNavigationTarget = this._matchesNavigationTarget(finalId);
|
|
2496
|
+
const shouldCommit = (finalId || root) &&
|
|
2497
|
+
(!alreadyMatches || needsIdInQuery || rootChanged || force ||
|
|
2498
|
+
(isNavigationActive && matchesNavigationTarget));
|
|
2499
|
+
|
|
2500
|
+
if (shouldCommit) {
|
|
2501
|
+
// Check if we should skip (recently committed same ID and not intentional navigation)
|
|
2502
|
+
const recentId = this._getRecentlyCommittedId();
|
|
2503
|
+
const shouldSkip = recentId && finalId === recentId &&
|
|
2504
|
+
!rootChanged && // Don't skip if root changed
|
|
2505
|
+
!isNavigationActive && // Don't skip if navigation is active
|
|
2506
|
+
!force; // Don't skip if force=true (intentional navigation)
|
|
2507
|
+
|
|
2508
|
+
if (!shouldSkip) {
|
|
2509
|
+
// Update navigation state if not already set (for user-initiated commits)
|
|
2510
|
+
if (!isNavigationActive && force) {
|
|
2511
|
+
this._setNavigationState('user', null, finalId);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
registry.commit(this, {
|
|
2515
|
+
id: finalId || null,
|
|
2516
|
+
root: root || null,
|
|
2517
|
+
});
|
|
2518
|
+
|
|
2519
|
+
// Verify registry state was actually committed
|
|
2520
|
+
setTimeout(() => {
|
|
2521
|
+
const committedState = registry.getState(this);
|
|
2522
|
+
if (committedState.id !== finalId || committedState.root !== root) {
|
|
2523
|
+
console.warn('[pb-tify] Registry state mismatch after commit', {
|
|
2524
|
+
expectedId: finalId,
|
|
2525
|
+
expectedRoot: root,
|
|
2526
|
+
actualId: committedState.id,
|
|
2527
|
+
actualRoot: committedState.root
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}, 50);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// Always update hash fragment if we have one
|
|
2535
|
+
// This ensures the URL always contains page information
|
|
2536
|
+
if (hashFragment) {
|
|
2537
|
+
// Update hash directly - registry doesn't handle hash updates
|
|
2538
|
+
const currentUrl = new URL(window.location.href);
|
|
2539
|
+
currentUrl.hash = hashFragment;
|
|
2540
|
+
// Use replaceState to update hash without adding history entry
|
|
2541
|
+
// This keeps the URL in sync with the page
|
|
2542
|
+
window.history.replaceState(window.history.state, '', currentUrl.toString());
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Always emit pb-refresh after updating URL (for Option 6: Hybrid approach)
|
|
2546
|
+
// This ensures pb-view gets notified even if it's debouncing registry changes
|
|
2547
|
+
// Since we now wait for Tify to change before committing, we can emit immediately
|
|
2548
|
+
// But still use a small delay to ensure registry state is fully updated
|
|
2549
|
+
setTimeout(() => {
|
|
2550
|
+
// Get the current canvas from Tify's viewer state to ensure accuracy
|
|
2551
|
+
let currentCanvas = canvas;
|
|
2552
|
+
|
|
2553
|
+
if (this._tify && this._tify.viewer) {
|
|
2554
|
+
const viewer = this._tify.viewer;
|
|
2555
|
+
if (viewer.currentCanvas) {
|
|
2556
|
+
currentCanvas = viewer.currentCanvas;
|
|
2557
|
+
} else if (viewer.currentPage) {
|
|
2558
|
+
const currentPage = typeof viewer.currentPage === 'function'
|
|
2559
|
+
? viewer.currentPage()
|
|
2560
|
+
: viewer.currentPage;
|
|
2561
|
+
if (currentPage !== undefined && currentPage !== null) {
|
|
2562
|
+
const root = this._getRootFromApp();
|
|
2563
|
+
if (root) {
|
|
2564
|
+
const canvases = this._getCanvases(root);
|
|
2565
|
+
const pageIndex = typeof currentPage === 'number' ? currentPage - 1 : currentPage;
|
|
2566
|
+
if (pageIndex >= 0 && pageIndex < canvases.length) {
|
|
2567
|
+
currentCanvas = canvases[pageIndex];
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
} else if (viewer._sequenceIndex !== undefined) {
|
|
2572
|
+
const root = this._getRootFromApp();
|
|
2573
|
+
if (root) {
|
|
2574
|
+
const canvases = this._getCanvases(root);
|
|
2575
|
+
if (viewer._sequenceIndex >= 0 && viewer._sequenceIndex < canvases.length) {
|
|
2576
|
+
currentCanvas = canvases[viewer._sequenceIndex];
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// Verify registry state matches what we're emitting
|
|
2583
|
+
// This ensures pb-view gets the correct state
|
|
2584
|
+
const registryState = registry.getState(this);
|
|
2585
|
+
const { rendering } = currentCanvas || canvas;
|
|
2586
|
+
let canvasId = null;
|
|
2587
|
+
if (rendering && rendering.length > 0) {
|
|
2588
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
2589
|
+
if (renderingId) {
|
|
2590
|
+
try {
|
|
2591
|
+
const url = new URL(renderingId);
|
|
2592
|
+
canvasId = url.searchParams.get('id');
|
|
2593
|
+
} catch (e) {
|
|
2594
|
+
// Ignore URL parsing errors
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// If registry state doesn't match yet, wait a bit more
|
|
2600
|
+
// This is important because pb-view prioritizes registry state over event detail
|
|
2601
|
+
// NOTE: With readOnlyRegistry enabled on pb-view, this should rarely happen
|
|
2602
|
+
// as pb-view won't reset the registry anymore
|
|
2603
|
+
if (canvasId && registryState.id !== canvasId) {
|
|
2604
|
+
// Wait a bit more for registry to stabilize
|
|
2605
|
+
setTimeout(() => {
|
|
2606
|
+
this._emitPbRefresh(currentCanvas || canvas);
|
|
2607
|
+
}, 150);
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// Registry state matches - emit refresh immediately
|
|
2612
|
+
this._emitPbRefresh(currentCanvas || canvas);
|
|
2613
|
+
}, 150); // Increased delay to ensure registry commit has fully processed
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
/**
|
|
2617
|
+
* Handle URL changes (e.g., from browser back/forward).
|
|
2618
|
+
* Called by registry subscription when URL changes.
|
|
2619
|
+
* @param {Object} state - The new state from registry
|
|
2620
|
+
*/
|
|
2621
|
+
_handleUrlChange(state) {
|
|
2622
|
+
if (!this._tify || !this._tify.app || !this._setPage) {
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// Check if navigation is active (any source except 'url')
|
|
2627
|
+
if (this._isNavigationActive()) {
|
|
2628
|
+
// If this URL change matches our navigation target, skip (we're navigating to it)
|
|
2629
|
+
if (this._matchesNavigationTarget(state.id)) {
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
// If another source is navigating (not 'url'), skip (they control navigation)
|
|
2633
|
+
if (this._navigationState.source !== 'url') {
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
// Check if we recently committed this page (post-navigation protection)
|
|
2639
|
+
if (this._isRecentlyCommitted(state.id)) {
|
|
2640
|
+
return; // This is our own commit, don't reset Tify
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// CRITICAL CHECK: Verify the state hasn't changed since this callback was queued
|
|
2644
|
+
// Registry callbacks can be delayed, so the state might have changed
|
|
2645
|
+
const currentRegistryState = registry.getState(this);
|
|
2646
|
+
if (currentRegistryState.id !== state.id || currentRegistryState.root !== state.root) {
|
|
2647
|
+
// State has changed - this callback is stale, ignore it
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const root = this._getRootFromApp();
|
|
2652
|
+
if (!root) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
const canvases = this._getCanvases(root);
|
|
2657
|
+
if (canvases.length === 0) {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
// PRIORITY 1: Check hash first (most reliable - e.g., #A-N-38_004.jpg)
|
|
2662
|
+
let targetPage = null;
|
|
2663
|
+
const hash = window.location.hash;
|
|
2664
|
+
if (hash) {
|
|
2665
|
+
const hashMatch = hash.match(/_(\d{2,3})\./);
|
|
2666
|
+
if (hashMatch) {
|
|
2667
|
+
const pageNum = parseInt(hashMatch[1], 10);
|
|
2668
|
+
if (pageNum >= 1 && pageNum <= canvases.length) {
|
|
2669
|
+
targetPage = pageNum;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// PRIORITY 2: Find page from state.id (registry is single source of truth)
|
|
2675
|
+
if (!targetPage && state.id) {
|
|
2676
|
+
for (let i = 0; i < canvases.length; i++) {
|
|
2677
|
+
const canvas = canvases[i];
|
|
2678
|
+
const { rendering } = canvas;
|
|
143
2679
|
if (rendering && rendering.length > 0) {
|
|
144
|
-
|
|
145
|
-
|
|
2680
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
2681
|
+
if (renderingId) {
|
|
2682
|
+
try {
|
|
2683
|
+
const url = new URL(renderingId);
|
|
2684
|
+
const canvasId = url.searchParams.get('id');
|
|
2685
|
+
if (canvasId === state.id) {
|
|
2686
|
+
targetPage = i + 1;
|
|
2687
|
+
break;
|
|
2688
|
+
}
|
|
2689
|
+
} catch (e) {
|
|
2690
|
+
// Ignore
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
// Also check canvas label for id match (e.g., A-N-38_002.jpg)
|
|
2695
|
+
if (canvas.label) {
|
|
2696
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
2697
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
2698
|
+
if (labelStr) {
|
|
2699
|
+
// Exact match or substring match
|
|
2700
|
+
if (state.id === labelStr || state.id.includes(labelStr) || labelStr.includes(state.id)) {
|
|
2701
|
+
targetPage = i + 1;
|
|
2702
|
+
break;
|
|
2703
|
+
}
|
|
2704
|
+
// Also try extracting page number from both
|
|
2705
|
+
const statePageMatch = state.id.match(/_(\d{2,3})\./);
|
|
2706
|
+
const labelPageMatch = labelStr.match(/(\d{2,3})/);
|
|
2707
|
+
if (statePageMatch && labelPageMatch && statePageMatch[1] === labelPageMatch[1]) {
|
|
2708
|
+
targetPage = i + 1;
|
|
2709
|
+
break;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
if (!targetPage) {
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
// CRITICAL: Check if Tify is already on the correct page BEFORE any other logic
|
|
2721
|
+
// This prevents unnecessary resets that could cause bounce-back
|
|
2722
|
+
let tifyCurrentPage = null;
|
|
2723
|
+
if (this._tify && this._tify.viewer) {
|
|
2724
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
2725
|
+
try {
|
|
2726
|
+
const currentTifyPage = this._tify.viewer.currentPage();
|
|
2727
|
+
if (typeof currentTifyPage === 'number') {
|
|
2728
|
+
tifyCurrentPage = currentTifyPage + 1;
|
|
2729
|
+
}
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
// Ignore
|
|
2732
|
+
}
|
|
2733
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
2734
|
+
const seqIndex = this._tify.viewer._sequenceIndex;
|
|
2735
|
+
if (typeof seqIndex === 'number') {
|
|
2736
|
+
tifyCurrentPage = seqIndex + 1;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// If already on correct page, just emit refresh and return
|
|
2742
|
+
// This is a critical guardrail - don't reset Tify if it's already correct
|
|
2743
|
+
if (tifyCurrentPage === targetPage) {
|
|
2744
|
+
this._currentPage = targetPage;
|
|
2745
|
+
this._emitPbRefresh(canvases[targetPage - 1]);
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// This should never be reached if _lastCommittedId matches (we return early above)
|
|
2750
|
+
// But add extra safety check for _targetPageId
|
|
2751
|
+
if (this._targetPageId && state.id === this._targetPageId) {
|
|
2752
|
+
// This is our target page - don't interfere, just emit refresh if needed
|
|
2753
|
+
this._emitPbRefresh(canvases[targetPage - 1]);
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// EXTRA PROTECTION: Check if Tify's current canvas matches the state we're trying to navigate to
|
|
2758
|
+
// This prevents resetting Tify if it's already showing the correct content, even if page numbers differ
|
|
2759
|
+
if (tifyCurrentPage && tifyCurrentPage > 0 && tifyCurrentPage <= canvases.length) {
|
|
2760
|
+
const currentCanvas = canvases[tifyCurrentPage - 1];
|
|
2761
|
+
if (currentCanvas) {
|
|
2762
|
+
// Check if current canvas matches the target state
|
|
2763
|
+
let currentCanvasId = null;
|
|
2764
|
+
const { rendering } = currentCanvas;
|
|
2765
|
+
if (rendering && rendering.length > 0) {
|
|
2766
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
2767
|
+
if (renderingId) {
|
|
2768
|
+
try {
|
|
2769
|
+
const url = new URL(renderingId, window.location.origin);
|
|
2770
|
+
currentCanvasId = url.searchParams.get('id');
|
|
2771
|
+
} catch (e) {
|
|
2772
|
+
// Ignore
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
// Also check label
|
|
2777
|
+
if (!currentCanvasId && currentCanvas.label) {
|
|
2778
|
+
const label = currentCanvas.label.none || currentCanvas.label.en || currentCanvas.label;
|
|
2779
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
2780
|
+
if (labelStr && state.id) {
|
|
2781
|
+
const pathParts = window.location.pathname.split('/');
|
|
2782
|
+
let docId = null;
|
|
2783
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
2784
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
2785
|
+
docId = pathParts[i];
|
|
2786
|
+
break;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
if (docId) {
|
|
2790
|
+
const pageNum = String(labelStr).padStart(3, '0');
|
|
2791
|
+
currentCanvasId = `${docId}_${pageNum}.jpg`;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// If current canvas matches the state, don't reset Tify
|
|
2797
|
+
if (currentCanvasId && state.id && currentCanvasId === state.id) {
|
|
2798
|
+
this._currentPage = tifyCurrentPage;
|
|
2799
|
+
this._emitPbRefresh(currentCanvas);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// Check if another navigation source is active
|
|
2806
|
+
if (this._isNavigationActive() && this._navigationState.source !== 'url') {
|
|
2807
|
+
return; // Another source is navigating, don't interfere
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// Set navigation state for URL-based navigation
|
|
2811
|
+
this._setNavigationState('url', targetPage, state.id);
|
|
2812
|
+
|
|
2813
|
+
// Update Tify to match registry
|
|
2814
|
+
this._setPage([targetPage]);
|
|
2815
|
+
this._currentPage = targetPage;
|
|
2816
|
+
|
|
2817
|
+
// Wait for Tify to actually change pages before emitting refresh
|
|
2818
|
+
const verifyAndEmit = (attempts = 0) => {
|
|
2819
|
+
let tifyPage = null;
|
|
2820
|
+
if (this._tify && this._tify.viewer) {
|
|
2821
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
2822
|
+
try {
|
|
2823
|
+
const page = this._tify.viewer.currentPage();
|
|
2824
|
+
if (typeof page === 'number') {
|
|
2825
|
+
tifyPage = page + 1;
|
|
2826
|
+
}
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
// Ignore
|
|
2829
|
+
}
|
|
2830
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
2831
|
+
const seqIndex = this._tify.viewer._sequenceIndex;
|
|
2832
|
+
if (typeof seqIndex === 'number') {
|
|
2833
|
+
tifyPage = seqIndex + 1;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
if (tifyPage === targetPage) {
|
|
2839
|
+
// Tify is on the correct page - emit refresh
|
|
2840
|
+
this._clearNavigationState();
|
|
2841
|
+
this._emitPbRefresh(canvases[targetPage - 1]);
|
|
2842
|
+
} else if (attempts < 10) {
|
|
2843
|
+
// Not yet, retry
|
|
2844
|
+
setTimeout(() => verifyAndEmit(attempts + 1), 200);
|
|
2845
|
+
} else {
|
|
2846
|
+
// Give up - emit refresh anyway
|
|
2847
|
+
this._clearNavigationState();
|
|
2848
|
+
this._emitPbRefresh(canvases[targetPage - 1]);
|
|
2849
|
+
}
|
|
2850
|
+
};
|
|
2851
|
+
|
|
2852
|
+
setTimeout(() => verifyAndEmit(), 200);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
/**
|
|
2856
|
+
* Emit pb-refresh event for transcription synchronization
|
|
2857
|
+
* @param {Object} canvas - The canvas object
|
|
2858
|
+
*/
|
|
2859
|
+
_emitPbRefresh(canvas) {
|
|
2860
|
+
// Get registry state first (always available and up-to-date)
|
|
2861
|
+
const registryState = registry.getState(this);
|
|
2862
|
+
|
|
2863
|
+
// Try to get params from canvas rendering URL
|
|
2864
|
+
let params = {};
|
|
2865
|
+
if (canvas) {
|
|
2866
|
+
const { rendering } = canvas;
|
|
2867
|
+
if (rendering && rendering.length > 0) {
|
|
2868
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
2869
|
+
if (renderingId) {
|
|
2870
|
+
try {
|
|
2871
|
+
// Handle both absolute and relative URLs
|
|
2872
|
+
let url;
|
|
2873
|
+
if (renderingId.startsWith('http://') || renderingId.startsWith('https://')) {
|
|
2874
|
+
url = new URL(renderingId);
|
|
2875
|
+
} else {
|
|
2876
|
+
// Relative URL - convert to absolute using current origin
|
|
2877
|
+
url = new URL(renderingId, window.location.origin);
|
|
2878
|
+
}
|
|
146
2879
|
url.searchParams.forEach((value, key) => {
|
|
147
|
-
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
|
|
2880
|
+
params[key] = value;
|
|
2881
|
+
});
|
|
2882
|
+
} catch (error) {
|
|
2883
|
+
console.warn('[pb-tify] _emitPbRefresh: Error parsing rendering URL:', renderingId, error);
|
|
2884
|
+
}
|
|
151
2885
|
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
// ALWAYS include root from registry state, even if we got params from canvas
|
|
2890
|
+
// This ensures pb-view gets the correct root value, which is critical for transcription sync
|
|
2891
|
+
// The registry state is the source of truth after _updateUrlFromPage has committed
|
|
2892
|
+
if (registryState.root) {
|
|
2893
|
+
params.root = registryState.root;
|
|
152
2894
|
}
|
|
2895
|
+
|
|
2896
|
+
// If no params from canvas, also include id from registry state
|
|
2897
|
+
// This is a fallback when canvas doesn't have rendering property
|
|
2898
|
+
if (!params.id && registryState.id) {
|
|
2899
|
+
params.id = registryState.id;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// If we still have no params, we can't emit a meaningful event
|
|
2903
|
+
if (Object.keys(params).length === 0) {
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
try {
|
|
2908
|
+
// Include registry state in event detail to ensure pb-view has the latest state
|
|
2909
|
+
// pb-view prioritizes registry state, so we need to make sure it's included
|
|
2910
|
+
|
|
2911
|
+
// Get channels - use emit attribute if set, otherwise use defaultChannel
|
|
2912
|
+
// Application code should specify emit="transcription" (or other channel) via HTML attribute
|
|
2913
|
+
// This follows the pb-mixin pattern where components are generic and applications configure them
|
|
2914
|
+
const channels = getEmittedChannels(this);
|
|
2915
|
+
|
|
2916
|
+
// Merge params with registry state for the event detail
|
|
2917
|
+
const eventDetail = Object.assign({}, params, {
|
|
2918
|
+
...registryState, // Include registry state so pb-view gets the correct state
|
|
2919
|
+
_source: this
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
// Use emitTo which properly handles channels and ensures pb-view receives the event
|
|
2923
|
+
// emitTo dispatches from the element, but events bubble to document where subscribeTo listens
|
|
2924
|
+
// emitTo will set the correct 'key' in the event detail based on the channel
|
|
2925
|
+
this.emitTo('pb-refresh', eventDetail, channels);
|
|
2926
|
+
|
|
2927
|
+
// Also dispatch directly to document to ensure it's caught by document.addEventListener
|
|
2928
|
+
// This is a fallback in case bubbling doesn't work as expected
|
|
2929
|
+
// Use the first channel from getEmittedChannels (or defaultChannel if none)
|
|
2930
|
+
const channelKey = channels[0] || defaultChannel;
|
|
2931
|
+
const directEventDetail = Object.assign({}, eventDetail, { key: channelKey });
|
|
2932
|
+
const directEv = new CustomEvent('pb-refresh', {
|
|
2933
|
+
detail: directEventDetail,
|
|
2934
|
+
composed: true,
|
|
2935
|
+
bubbles: true,
|
|
2936
|
+
});
|
|
2937
|
+
document.dispatchEvent(directEv);
|
|
2938
|
+
} catch (error) {
|
|
2939
|
+
console.error('<pb-tify> Error emitting pb-refresh:', error);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
153
2942
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
2943
|
+
/**
|
|
2944
|
+
* Get the canvas array from the manifest, supporting both IIIF 2.0 and 3.0 formats.
|
|
2945
|
+
* @param {Object} root - The root manifest object
|
|
2946
|
+
* @returns {Array} Array of canvases
|
|
2947
|
+
*/
|
|
2948
|
+
_getCanvases(root) {
|
|
2949
|
+
if (!root) {
|
|
2950
|
+
return [];
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// Helper to check if an item is a Canvas
|
|
2954
|
+
const isCanvas = (item) => {
|
|
2955
|
+
return item && (
|
|
2956
|
+
item.type === 'Canvas' ||
|
|
2957
|
+
item['@type'] === 'sc:Canvas' ||
|
|
2958
|
+
item['@type'] === 'Canvas'
|
|
2959
|
+
);
|
|
2960
|
+
};
|
|
2961
|
+
|
|
2962
|
+
// Helper to recursively collect all canvases from items
|
|
2963
|
+
const collectCanvases = (items) => {
|
|
2964
|
+
if (!Array.isArray(items)) {
|
|
2965
|
+
return [];
|
|
2966
|
+
}
|
|
2967
|
+
const canvases = [];
|
|
2968
|
+
for (const item of items) {
|
|
2969
|
+
if (isCanvas(item)) {
|
|
2970
|
+
canvases.push(item);
|
|
2971
|
+
} else if (item.items && Array.isArray(item.items)) {
|
|
2972
|
+
// Recursively search in nested items (for sequences, ranges, etc.)
|
|
2973
|
+
canvases.push(...collectCanvases(item.items));
|
|
158
2974
|
}
|
|
2975
|
+
}
|
|
2976
|
+
return canvases;
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2979
|
+
// IIIF 3.0: canvases are in items (directly or nested in sequences/ranges)
|
|
2980
|
+
if (root.items && Array.isArray(root.items)) {
|
|
2981
|
+
const canvases = collectCanvases(root.items);
|
|
2982
|
+
if (canvases.length > 0) {
|
|
2983
|
+
return canvases;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// IIIF 2.0: canvases are in sequences[0].canvases
|
|
2988
|
+
if (root.sequences && Array.isArray(root.sequences) && root.sequences.length > 0) {
|
|
2989
|
+
const canvases = root.sequences[0].canvases || [];
|
|
2990
|
+
if (canvases.length > 0) {
|
|
2991
|
+
return canvases;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// Fallback: try canvases directly (some IIIF 2.0 variants)
|
|
2996
|
+
if (root.canvases && Array.isArray(root.canvases)) {
|
|
2997
|
+
return root.canvases;
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
return [];
|
|
3001
|
+
}
|
|
159
3002
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
3003
|
+
_getRootFromApp() {
|
|
3004
|
+
// Try multiple ways to get the manifest/root data from Tify
|
|
3005
|
+
if (!this._tify) {
|
|
3006
|
+
return this._cachedManifest || null;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
// First try to get from app object (Vue 2/3 style)
|
|
3010
|
+
if (this._tify.app) {
|
|
3011
|
+
const app = this._tify.app;
|
|
3012
|
+
|
|
3013
|
+
// Vue 2 style: $root, $data
|
|
3014
|
+
const root = app.$root || app.root || app.manifest || (app.$data && app.$data.root) || (app.$data && app.$data.manifest);
|
|
3015
|
+
if (root) {
|
|
3016
|
+
return root;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
// Vue 3: Try _context.app (root app instance)
|
|
3020
|
+
if (app._context && app._context.app) {
|
|
3021
|
+
const rootApp = app._context.app;
|
|
3022
|
+
const rootFromContext = rootApp.$root || rootApp.root || rootApp.manifest ||
|
|
3023
|
+
(rootApp.$data && rootApp.$data.root) ||
|
|
3024
|
+
(rootApp.$data && rootApp.$data.manifest);
|
|
3025
|
+
if (rootFromContext) {
|
|
3026
|
+
return rootFromContext;
|
|
166
3027
|
}
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
// Vue 3: Try _context directly
|
|
3031
|
+
if (app._context) {
|
|
3032
|
+
const rootFromContext = app._context.root || app._context.manifest;
|
|
3033
|
+
if (rootFromContext) {
|
|
3034
|
+
return rootFromContext;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// Vue 3: Try from app's internal state (composition API)
|
|
3039
|
+
if (app._instance && app._instance.setupState) {
|
|
3040
|
+
const setupState = app._instance.setupState;
|
|
3041
|
+
if (setupState.root) return setupState.root;
|
|
3042
|
+
if (setupState.manifest) return setupState.manifest;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
// Vue 3: Try _instance.exposed (exposed properties)
|
|
3046
|
+
if (app._instance && app._instance.exposed) {
|
|
3047
|
+
const exposed = app._instance.exposed;
|
|
3048
|
+
if (exposed.root) return exposed.root;
|
|
3049
|
+
if (exposed.manifest) return exposed.manifest;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
// Try direct access from Tify instance
|
|
3054
|
+
if (this._tify.manifest) {
|
|
3055
|
+
return this._tify.manifest;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
// Try from viewer
|
|
3059
|
+
if (this._tify.viewer && this._tify.viewer.manifest) {
|
|
3060
|
+
return this._tify.viewer.manifest;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
// Try from Tify's internal store or state
|
|
3064
|
+
if (this._tify.store && this._tify.store.state) {
|
|
3065
|
+
const state = this._tify.store.state;
|
|
3066
|
+
if (state.root) return state.root;
|
|
3067
|
+
if (state.manifest) return state.manifest;
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
// Fallback: use cached manifest if available
|
|
3071
|
+
return this._cachedManifest || null;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
async _fetchAndCacheManifest() {
|
|
3075
|
+
// Fetch manifest directly from URL as fallback
|
|
3076
|
+
if (this._manifestUrl && !this._cachedManifest) {
|
|
3077
|
+
try {
|
|
3078
|
+
const response = await fetch(this._manifestUrl);
|
|
3079
|
+
this._cachedManifest = await response.json();
|
|
3080
|
+
return this._cachedManifest;
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
console.warn('<pb-tify> Failed to fetch manifest as fallback:', error);
|
|
3083
|
+
return null;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
return this._cachedManifest || null;
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
/**
|
|
3090
|
+
* Fetch the page-to-root mapping from the backend API.
|
|
3091
|
+
* This map allows us to get the root identifier (node ID format, e.g., "1.7.2.2.4")
|
|
3092
|
+
* for any page number.
|
|
3093
|
+
* @returns {Promise<Object>} Map of page numbers (e.g., "011") to root values (e.g., "1.7.2.2.17")
|
|
3094
|
+
*/
|
|
3095
|
+
async _fetchPageToRootMap() {
|
|
3096
|
+
// Extract document path from URL
|
|
3097
|
+
const pathParts = window.location.pathname.split('/');
|
|
3098
|
+
let docPath = null;
|
|
3099
|
+
for (let i = pathParts.length - 1; i >= 0; i--) {
|
|
3100
|
+
if (pathParts[i] && pathParts[i] !== 'apps' && pathParts[i] !== 'exist') {
|
|
3101
|
+
docPath = pathParts[i];
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (!docPath) {
|
|
3107
|
+
console.warn('[pb-tify] _fetchPageToRootMap: could not extract document path from URL');
|
|
3108
|
+
return null;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
const endpoint = this.getEndpoint();
|
|
3112
|
+
// In component test environments, endpoint might be undefined
|
|
3113
|
+
// Use window.location.origin as fallback (same as the page's origin)
|
|
3114
|
+
const effectiveEndpoint = endpoint || window.location.origin;
|
|
3115
|
+
// Use the new dedicated roots endpoint (Option 2) - much more efficient than parsing full manifest
|
|
3116
|
+
const rootsUrl = `${effectiveEndpoint}/api/iiif/${encodeURIComponent(docPath)}/roots`;
|
|
3117
|
+
|
|
3118
|
+
try {
|
|
3119
|
+
const response = await fetch(rootsUrl);
|
|
3120
|
+
if (!response.ok) {
|
|
3121
|
+
console.warn('[pb-tify] _fetchPageToRootMap: failed to fetch roots map', {
|
|
3122
|
+
status: response.status,
|
|
3123
|
+
statusText: response.statusText,
|
|
3124
|
+
url: rootsUrl
|
|
3125
|
+
});
|
|
3126
|
+
return null;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
const pageToRootMap = await response.json();
|
|
3130
|
+
return pageToRootMap;
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
console.warn('[pb-tify] _fetchPageToRootMap: failed to fetch or parse roots map', error);
|
|
3133
|
+
return null;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
/**
|
|
3138
|
+
* Ensure the page-to-root lookup map is available.
|
|
3139
|
+
* Fetches it once and caches it for subsequent use.
|
|
3140
|
+
* @returns {Promise<void>}
|
|
3141
|
+
*/
|
|
3142
|
+
async _ensurePageToRootMap() {
|
|
3143
|
+
// If already cached, return immediately
|
|
3144
|
+
if (this._pageToRootMap) {
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
// If already fetching, wait for it to complete
|
|
3149
|
+
if (this._fetchingPageToRootMap) {
|
|
3150
|
+
// Wait for the fetch to complete (polling)
|
|
3151
|
+
let attempts = 0;
|
|
3152
|
+
while (this._fetchingPageToRootMap && attempts < 50) {
|
|
3153
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
3154
|
+
attempts++;
|
|
3155
|
+
}
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// Start fetching
|
|
3160
|
+
this._fetchingPageToRootMap = true;
|
|
3161
|
+
try {
|
|
3162
|
+
this._pageToRootMap = await this._fetchPageToRootMap();
|
|
3163
|
+
} finally {
|
|
3164
|
+
this._fetchingPageToRootMap = false;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
167
3167
|
|
|
168
|
-
|
|
3168
|
+
/**
|
|
3169
|
+
* Handle navigation to next/previous page
|
|
3170
|
+
* @private
|
|
3171
|
+
*/
|
|
3172
|
+
async _handleNavigate(direction) {
|
|
3173
|
+
const root = this._getRootFromApp();
|
|
3174
|
+
|
|
3175
|
+
if (!this._tify || !this._tify.app) {
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
// If root is not available yet, wait for it and retry
|
|
3180
|
+
if (!root) {
|
|
3181
|
+
setTimeout(() => {
|
|
3182
|
+
this._handleNavigate(direction);
|
|
3183
|
+
}, 100);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// root was already declared above, reuse it
|
|
3188
|
+
const canvases = this._getCanvases(root);
|
|
3189
|
+
const totalPages = canvases.length;
|
|
3190
|
+
|
|
3191
|
+
if (totalPages === 0) {
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// Get current page from registry (single source of truth)
|
|
3196
|
+
const state = registry.getState(this);
|
|
3197
|
+
let currentPage = null;
|
|
3198
|
+
|
|
3199
|
+
// Find page from state.id
|
|
3200
|
+
if (state.id) {
|
|
3201
|
+
for (let i = 0; i < canvases.length; i++) {
|
|
3202
|
+
const canvas = canvases[i];
|
|
3203
|
+
const { rendering } = canvas;
|
|
3204
|
+
if (rendering && rendering.length > 0) {
|
|
3205
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
3206
|
+
if (renderingId) {
|
|
3207
|
+
try {
|
|
3208
|
+
const url = new URL(renderingId);
|
|
3209
|
+
const canvasId = url.searchParams.get('id');
|
|
3210
|
+
if (canvasId === state.id) {
|
|
3211
|
+
currentPage = i + 1;
|
|
3212
|
+
break;
|
|
3213
|
+
}
|
|
3214
|
+
} catch (e) {
|
|
3215
|
+
// Ignore
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
// Also check canvas label
|
|
3220
|
+
if (canvas.label) {
|
|
3221
|
+
const label = canvas.label.none || canvas.label.en || canvas.label;
|
|
3222
|
+
const labelStr = Array.isArray(label) ? label[0] : label;
|
|
3223
|
+
if (labelStr && state.id.includes(labelStr)) {
|
|
3224
|
+
currentPage = i + 1;
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
// Fallback to Tify's state or internal state
|
|
3232
|
+
if (!currentPage) {
|
|
3233
|
+
if (this._tify && this._tify.viewer) {
|
|
3234
|
+
if (typeof this._tify.viewer.currentPage === 'function') {
|
|
3235
|
+
try {
|
|
3236
|
+
const currentTifyPage = this._tify.viewer.currentPage();
|
|
3237
|
+
if (typeof currentTifyPage === 'number') {
|
|
3238
|
+
currentPage = currentTifyPage + 1;
|
|
3239
|
+
}
|
|
3240
|
+
} catch (e) {
|
|
3241
|
+
// Ignore
|
|
3242
|
+
}
|
|
3243
|
+
} else if (this._tify.viewer._sequenceIndex !== undefined) {
|
|
3244
|
+
const seqIndex = this._tify.viewer._sequenceIndex;
|
|
3245
|
+
if (typeof seqIndex === 'number') {
|
|
3246
|
+
currentPage = seqIndex + 1;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// Final fallback
|
|
3253
|
+
if (!currentPage) {
|
|
3254
|
+
currentPage = this._currentPage || 1;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
let newPage;
|
|
3258
|
+
if (direction === 'forward') {
|
|
3259
|
+
newPage = Math.min(currentPage + 1, totalPages);
|
|
3260
|
+
} else if (direction === 'backward') {
|
|
3261
|
+
newPage = Math.max(currentPage - 1, 1);
|
|
3262
|
+
} else {
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
169
3265
|
|
|
170
|
-
|
|
171
|
-
|
|
3266
|
+
// Update Tify directly, then update registry
|
|
3267
|
+
const canvas = canvases[newPage - 1];
|
|
3268
|
+
if (canvas) {
|
|
3269
|
+
// Extract target page ID
|
|
3270
|
+
const { rendering } = canvas;
|
|
3271
|
+
let targetId = null;
|
|
3272
|
+
if (rendering && rendering.length > 0) {
|
|
3273
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
3274
|
+
if (renderingId) {
|
|
3275
|
+
try {
|
|
3276
|
+
const url = new URL(renderingId);
|
|
3277
|
+
targetId = url.searchParams.get('id');
|
|
3278
|
+
} catch (e) {
|
|
3279
|
+
// Ignore
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// Set navigation state for programmatic navigation
|
|
3285
|
+
this._setNavigationState('programmatic', newPage, targetId);
|
|
3286
|
+
|
|
3287
|
+
// Update Tify directly first, then update registry
|
|
3288
|
+
if (this._setPage) {
|
|
3289
|
+
this._setPage([newPage]);
|
|
3290
|
+
|
|
3291
|
+
// Wait a bit for Tify to update, then update registry
|
|
3292
|
+
setTimeout(async () => {
|
|
3293
|
+
await this._updateUrlFromPage(canvas, true);
|
|
3294
|
+
|
|
3295
|
+
// Mark navigation complete
|
|
3296
|
+
if (targetId) {
|
|
3297
|
+
this._markNavigationComplete(targetId);
|
|
3298
|
+
}
|
|
3299
|
+
setTimeout(() => {
|
|
3300
|
+
this._clearNavigationState();
|
|
3301
|
+
}, 500);
|
|
3302
|
+
}, 100);
|
|
3303
|
+
} else {
|
|
3304
|
+
// Fallback: just update registry
|
|
3305
|
+
await this._updateUrlFromPage(canvas, true);
|
|
3306
|
+
|
|
3307
|
+
// Mark navigation complete
|
|
3308
|
+
if (targetId) {
|
|
3309
|
+
this._markNavigationComplete(targetId);
|
|
3310
|
+
}
|
|
3311
|
+
setTimeout(() => {
|
|
3312
|
+
this._clearNavigationState();
|
|
3313
|
+
}, 500);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
172
3317
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
3318
|
+
async _switchPage(canvas) {
|
|
3319
|
+
// Update URL from canvas (single source of truth)
|
|
3320
|
+
// This triggers _handleUrlChange which updates Tify
|
|
3321
|
+
await this._updateUrlFromPage(canvas);
|
|
3322
|
+
|
|
3323
|
+
// Emit refresh event for transcription
|
|
3324
|
+
const { rendering } = canvas;
|
|
3325
|
+
if (rendering && rendering.length > 0) {
|
|
3326
|
+
const renderingId = rendering[0]['@id'] || rendering[0].id;
|
|
3327
|
+
if (renderingId) {
|
|
3328
|
+
try {
|
|
3329
|
+
const url = new URL(renderingId);
|
|
3330
|
+
const params = {};
|
|
3331
|
+
url.searchParams.forEach((value, key) => {
|
|
3332
|
+
params[key] = value;
|
|
3333
|
+
});
|
|
3334
|
+
|
|
3335
|
+
const channel = this.emit || this.getAttribute('emit') || 'transcription';
|
|
3336
|
+
const eventDetail = Object.assign({}, params, {
|
|
3337
|
+
key: channel,
|
|
3338
|
+
_source: this
|
|
3339
|
+
});
|
|
3340
|
+
|
|
3341
|
+
const ev = new CustomEvent('pb-refresh', {
|
|
3342
|
+
detail: eventDetail,
|
|
3343
|
+
composed: true,
|
|
3344
|
+
bubbles: true,
|
|
3345
|
+
});
|
|
3346
|
+
|
|
3347
|
+
document.dispatchEvent(ev);
|
|
3348
|
+
|
|
3349
|
+
// Use getEmittedChannels to respect emit attribute or defaultChannel
|
|
3350
|
+
const channels = getEmittedChannels(this);
|
|
3351
|
+
this.emitTo('pb-refresh', params, channels);
|
|
3352
|
+
} catch (error) {
|
|
3353
|
+
console.error('<pb-tify> Error parsing rendering URL:', renderingId, error);
|
|
176
3354
|
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
177
3358
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
3359
|
+
_addOverlay(coordinates) {
|
|
3360
|
+
if (!Array.isArray(coordinates) || coordinates.length !== 4) {
|
|
3361
|
+
console.error('coords incomplete or missing (array of 4 numbers expected)', coordinates);
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
const { viewer } = this._tify;
|
|
3366
|
+
const { viewport } = viewer;
|
|
3367
|
+
const overlayId = 'runtime-overlay';
|
|
3368
|
+
|
|
3369
|
+
if (this.overlay) {
|
|
3370
|
+
viewer.removeOverlay(this.overlay);
|
|
189
3371
|
}
|
|
190
3372
|
|
|
191
|
-
|
|
192
|
-
|
|
3373
|
+
const viewportBounds = viewport.getBounds();
|
|
3374
|
+
|
|
3375
|
+
const [x1, y1, w, h] = coordinates;
|
|
3376
|
+
const rect = viewport.imageToViewportRectangle(x1, y1, w, h);
|
|
3377
|
+
|
|
3378
|
+
// Scroll into view if necessary
|
|
3379
|
+
if (!viewportBounds.containsPoint(rect.getTopLeft())) {
|
|
3380
|
+
viewer.viewport.panTo(rect.getCenter());
|
|
193
3381
|
}
|
|
3382
|
+
|
|
3383
|
+
// Add overlay to viewer
|
|
3384
|
+
const overlay = document.createElement('div');
|
|
3385
|
+
this.overlay = overlay;
|
|
3386
|
+
overlay.id = overlayId;
|
|
3387
|
+
overlay.style.border = 'var(--pb-facsimile-border, none)';
|
|
3388
|
+
overlay.style.outline = 'var(--pb-facsimile-outline, 4px solid rgba(0, 0, 128, 0.5))';
|
|
3389
|
+
overlay.style.background = 'var(--pb-facsimile-background, rgba(0, 0, 128, 0.05))';
|
|
3390
|
+
viewer.addOverlay({
|
|
3391
|
+
element: overlay,
|
|
3392
|
+
location: rect,
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
createRenderRoot() {
|
|
3397
|
+
return this;
|
|
3398
|
+
}
|
|
194
3399
|
}
|
|
195
|
-
customElements.define('pb-tify', PbTify);
|
|
3400
|
+
customElements.define('pb-tify', PbTify);
|