@teipublisher/pb-components 2.26.1-next.3 → 3.0.0-next-4.1

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.
Files changed (262) hide show
  1. package/.github/workflows/docker-cypress.yml +54 -0
  2. package/.github/workflows/main.yml +6 -4
  3. package/.github/workflows/node.js.yml +56 -21
  4. package/.github/workflows/release.js.yml +19 -17
  5. package/.releaserc.json +1 -1
  6. package/CHANGELOG.md +346 -11
  7. package/Dockerfile +78 -70
  8. package/README.md +112 -4
  9. package/css/components.css +5 -5
  10. package/css/gridjs/mermaid.min.css +1 -1
  11. package/css/leaflet/Control.Geocoder.css +1 -126
  12. package/css/leaflet/images/layers.png +0 -0
  13. package/css/tify/tify.css +6 -5
  14. package/css/tom-select/tom-select.bootstrap4.min.css +1 -1
  15. package/css/tom-select/tom-select.bootstrap5.min.css +1 -1
  16. package/css/tom-select/tom-select.default.min.css +1 -1
  17. package/css/tom-select/tom-select.default.min.css.map +1 -0
  18. package/css/tom-select/tom-select.min.css +1 -1
  19. package/cypress.config.js +84 -0
  20. package/dist/api.html +1 -1
  21. package/dist/css/design-system.css +607 -0
  22. package/dist/demo/bundle-test.html +4 -3
  23. package/dist/demo/components.css +46 -1
  24. package/dist/demo/design-system.html +710 -0
  25. package/dist/demo/dts-client.html +2 -2
  26. package/dist/demo/pb-autocomplete.html +23 -11
  27. package/dist/demo/pb-autocomplete2.html +66 -55
  28. package/dist/demo/pb-autocomplete3.html +17 -8
  29. package/dist/demo/pb-blacklab-highlight.html +28 -11
  30. package/dist/demo/pb-blacklab-results.html +3 -2
  31. package/dist/demo/pb-browse-docs.html +24 -24
  32. package/dist/demo/pb-browse-docs2.html +3 -3
  33. package/dist/demo/pb-clipboard.html +32 -28
  34. package/dist/demo/pb-code-editor.html +6 -6
  35. package/dist/demo/pb-code-highlight.html +63 -63
  36. package/dist/demo/pb-codepen.html +1 -1
  37. package/dist/demo/pb-collapse.html +1 -1
  38. package/dist/demo/pb-collapse2.html +2 -2
  39. package/dist/demo/pb-combo-box.html +135 -130
  40. package/dist/demo/pb-custom-form.html +64 -55
  41. package/dist/demo/pb-dialog.html +12 -6
  42. package/dist/demo/pb-document.html +1 -1
  43. package/dist/demo/pb-download.html +68 -59
  44. package/dist/demo/pb-drawer.html +67 -46
  45. package/dist/demo/pb-drawer2.html +65 -58
  46. package/dist/demo/pb-edit-app.html +2 -2
  47. package/dist/demo/pb-edit-xml.html +1 -1
  48. package/dist/demo/pb-facsimile-2.html +26 -11
  49. package/dist/demo/pb-facsimile-3.html +25 -10
  50. package/dist/demo/pb-facsimile-dedup-test-2.html +48 -0
  51. package/dist/demo/pb-facsimile-dedup-test.html +48 -0
  52. package/dist/demo/pb-facsimile.html +4 -4
  53. package/dist/demo/pb-formula.html +1 -1
  54. package/dist/demo/pb-grid.html +22 -8
  55. package/dist/demo/pb-highlight.html +2 -2
  56. package/dist/demo/pb-i18n-simple.html +1 -0
  57. package/dist/demo/pb-i18n.html +15 -5
  58. package/dist/demo/pb-image-strip-standalone.html +2 -2
  59. package/dist/demo/pb-image-strip-view.html +2 -2
  60. package/dist/demo/pb-leaflet-map.html +3 -3
  61. package/dist/demo/pb-leaflet-map2.html +2 -2
  62. package/dist/demo/pb-leaflet-map3.html +3 -3
  63. package/dist/demo/pb-link.html +1 -1
  64. package/dist/demo/pb-load.html +2 -6
  65. package/dist/demo/pb-login.html +1 -3
  66. package/dist/demo/pb-manage-odds.html +9 -4
  67. package/dist/demo/pb-markdown.html +1 -1
  68. package/dist/demo/pb-media-query.html +2 -2
  69. package/dist/demo/pb-mei.html +2 -2
  70. package/dist/demo/pb-mei2.html +2 -2
  71. package/dist/demo/pb-message.html +2 -3
  72. package/dist/demo/pb-odd-editor.html +54 -52
  73. package/dist/demo/pb-page-header.html +27 -0
  74. package/dist/demo/pb-popover.html +1 -1
  75. package/dist/demo/pb-print-preview.html +2 -2
  76. package/dist/demo/pb-progress.html +4 -4
  77. package/dist/demo/pb-repeat.html +32 -36
  78. package/dist/demo/pb-search.html +16 -5
  79. package/dist/demo/pb-search2.html +4 -4
  80. package/dist/demo/pb-search3.html +3 -3
  81. package/dist/demo/pb-search4.html +3 -3
  82. package/dist/demo/pb-select-feature.html +4 -4
  83. package/dist/demo/pb-select-feature2.html +4 -4
  84. package/dist/demo/pb-select-feature3.html +2 -2
  85. package/dist/demo/pb-select-i18n.html +58 -53
  86. package/dist/demo/pb-select-odd.html +1 -1
  87. package/dist/demo/pb-select.html +190 -75
  88. package/dist/demo/pb-select2.html +91 -37
  89. package/dist/demo/pb-select3.html +109 -41
  90. package/dist/demo/pb-svg.html +1 -1
  91. package/dist/demo/pb-table-grid.html +26 -15
  92. package/dist/demo/pb-tabs.html +15 -7
  93. package/dist/demo/pb-tify.html +7 -7
  94. package/dist/demo/pb-timeline.html +1 -1
  95. package/dist/demo/pb-timeline2.html +1 -1
  96. package/dist/demo/pb-toggle-feature.html +26 -23
  97. package/dist/demo/pb-toggle-feature2.html +4 -4
  98. package/dist/demo/pb-toggle-feature3.html +2 -2
  99. package/dist/demo/pb-toggle-feature4.html +56 -54
  100. package/dist/demo/pb-version.html +2 -2
  101. package/dist/demo/pb-view.html +78 -40
  102. package/dist/demo/pb-view2.html +69 -46
  103. package/dist/demo/pb-view3.html +53 -48
  104. package/dist/demo/pb-view4.html +70 -49
  105. package/dist/demo/pb-zoom.html +2 -2
  106. package/dist/{es-global-bridge-d8ce175d.js → es-global-bridge-D8ZcUcx_.js} +0 -4
  107. package/dist/focus-mixin-VCsFap6b.js +768 -0
  108. package/dist/images/icons.svg +217 -0
  109. package/dist/jinn-codemirror-DETLdm08.js +1 -0
  110. package/dist/lib/openseadragon.min.js +80 -0
  111. package/dist/lib/openseadragon.min.js.map +1 -0
  112. package/dist/pb-code-editor.js +25 -20
  113. package/dist/pb-component-docs.js +414 -3225
  114. package/dist/pb-components-bundle.js +3046 -4402
  115. package/dist/pb-dialog-tklYGWfc.js +121 -0
  116. package/dist/pb-edit-app.js +208 -107
  117. package/dist/pb-elements.json +716 -249
  118. package/dist/pb-facsimile.js +46 -0
  119. package/dist/pb-i18n-C0NDma4h.js +1 -0
  120. package/dist/pb-leaflet-map.js +23 -23
  121. package/dist/pb-mei.js +152 -134
  122. package/dist/pb-mixin-DHoWQheB.js +1 -0
  123. package/dist/pb-odd-editor.js +1671 -1231
  124. package/dist/pb-tify.js +1 -27
  125. package/dist/unsafe-html-D5VGo9Oq.js +1 -0
  126. package/dist/urls-BEONu_g4.js +1 -0
  127. package/eslint.config.mjs +92 -0
  128. package/gh-pages.js +5 -3
  129. package/i18n/common/en.json +6 -0
  130. package/i18n/common/pl.json +2 -2
  131. package/images/icons.svg +217 -0
  132. package/index.html +0 -5
  133. package/lib/leaflet-src.js.map +1 -0
  134. package/lib/leaflet.markercluster-src.js.map +1 -0
  135. package/lib/openseadragon.min.js +6 -6
  136. package/package.json +56 -81
  137. package/pb-elements.json +716 -249
  138. package/rollup.config.mjs +312 -0
  139. package/src/assets/components.css +5 -5
  140. package/src/assets/design-system.css +607 -0
  141. package/src/authority/airtable.js +20 -21
  142. package/src/authority/anton.js +129 -129
  143. package/src/authority/custom.js +70 -27
  144. package/src/authority/geonames.js +38 -32
  145. package/src/authority/gnd.js +50 -42
  146. package/src/authority/kbga.js +136 -134
  147. package/src/authority/metagrid.js +44 -46
  148. package/src/authority/reconciliation.js +66 -68
  149. package/src/authority/registry.js +4 -4
  150. package/src/docs/demo-utils.js +91 -0
  151. package/src/docs/pb-component-docs.js +287 -147
  152. package/src/docs/pb-component-view.js +380 -273
  153. package/src/docs/pb-components-list.js +115 -51
  154. package/src/docs/pb-demo-snippet.js +199 -174
  155. package/src/dts-client.js +306 -303
  156. package/src/dts-select-endpoint.js +125 -85
  157. package/src/parse-date-service.js +184 -135
  158. package/src/pb-ajax.js +175 -173
  159. package/src/pb-authority-lookup.js +198 -158
  160. package/src/pb-autocomplete.js +731 -313
  161. package/src/pb-blacklab-highlight.js +266 -260
  162. package/src/pb-blacklab-results.js +230 -225
  163. package/src/pb-browse-docs.js +601 -484
  164. package/src/pb-browse.js +68 -65
  165. package/src/pb-clipboard.js +97 -76
  166. package/src/pb-code-editor.js +111 -103
  167. package/src/pb-code-highlight.js +234 -204
  168. package/src/pb-codepen.js +81 -73
  169. package/src/pb-collapse.js +265 -152
  170. package/src/pb-combo-box.js +191 -191
  171. package/src/pb-components-bundle.js +1 -7
  172. package/src/pb-components.js +2 -6
  173. package/src/pb-custom-form.js +230 -141
  174. package/src/pb-dialog.js +99 -63
  175. package/src/pb-document.js +118 -91
  176. package/src/pb-download.js +214 -198
  177. package/src/pb-drawer.js +146 -149
  178. package/src/pb-edit-app.js +471 -240
  179. package/src/pb-edit-xml.js +101 -98
  180. package/src/pb-events.js +126 -107
  181. package/src/pb-facs-link.js +130 -101
  182. package/src/pb-facsimile.js +494 -410
  183. package/src/pb-fetch.js +389 -0
  184. package/src/pb-formula.js +152 -154
  185. package/src/pb-geolocation.js +130 -132
  186. package/src/pb-grid-action.js +59 -56
  187. package/src/pb-grid.js +388 -228
  188. package/src/pb-highlight.js +142 -142
  189. package/src/pb-hotkeys.js +40 -42
  190. package/src/pb-i18n.js +115 -127
  191. package/src/pb-icon-button.js +108 -0
  192. package/src/pb-icon.js +283 -0
  193. package/src/pb-image-strip.js +85 -79
  194. package/src/pb-lang.js +142 -57
  195. package/src/pb-leaflet-map.js +551 -483
  196. package/src/pb-link.js +132 -126
  197. package/src/pb-load.js +495 -428
  198. package/src/pb-login.js +303 -248
  199. package/src/pb-manage-odds.js +384 -338
  200. package/src/pb-map-icon.js +90 -90
  201. package/src/pb-map-layer.js +86 -86
  202. package/src/pb-markdown.js +107 -110
  203. package/src/pb-media-query.js +75 -73
  204. package/src/pb-mei.js +523 -303
  205. package/src/pb-message.js +144 -98
  206. package/src/pb-mixin.js +268 -265
  207. package/src/pb-navigation.js +83 -96
  208. package/src/pb-observable.js +39 -39
  209. package/src/pb-odd-editor.js +1209 -948
  210. package/src/pb-odd-elementspec-editor.js +375 -310
  211. package/src/pb-odd-model-editor.js +1189 -941
  212. package/src/pb-odd-parameter-editor.js +269 -170
  213. package/src/pb-odd-rendition-editor.js +184 -131
  214. package/src/pb-page.js +451 -422
  215. package/src/pb-paginate.js +260 -178
  216. package/src/pb-panel.js +217 -183
  217. package/src/pb-popover-themes.js +16 -9
  218. package/src/pb-popover.js +297 -288
  219. package/src/pb-print-preview.js +128 -128
  220. package/src/pb-progress.js +52 -52
  221. package/src/pb-repeat.js +141 -108
  222. package/src/pb-restricted.js +85 -78
  223. package/src/pb-search.js +258 -230
  224. package/src/pb-select-feature.js +210 -126
  225. package/src/pb-select-odd.js +184 -118
  226. package/src/pb-select-template.js +113 -78
  227. package/src/pb-select.js +330 -229
  228. package/src/pb-split-list.js +181 -176
  229. package/src/pb-svg.js +81 -80
  230. package/src/pb-table-column.js +55 -55
  231. package/src/pb-table-grid.js +334 -205
  232. package/src/pb-tabs.js +238 -61
  233. package/src/pb-tify.js +3331 -126
  234. package/src/pb-timeline.js +394 -255
  235. package/src/pb-toggle-feature.js +196 -188
  236. package/src/pb-upload.js +201 -176
  237. package/src/pb-version.js +22 -34
  238. package/src/pb-view-annotate.js +138 -102
  239. package/src/pb-view.js +1722 -1272
  240. package/src/pb-zoom.js +144 -46
  241. package/src/search-result-service.js +256 -223
  242. package/src/seed-element.js +14 -22
  243. package/src/settings.js +4 -4
  244. package/src/theming.js +98 -91
  245. package/src/urls.js +403 -289
  246. package/src/utils.js +53 -51
  247. package/vite.config.js +86 -0
  248. package/css/pb-styles.css +0 -51
  249. package/dist/iron-form-3b8dcaa7.js +0 -210
  250. package/dist/jinn-codemirror-da0e2d1f.js +0 -1
  251. package/dist/paper-checkbox-515a5284.js +0 -1597
  252. package/dist/paper-icon-button-b1d31571.js +0 -398
  253. package/dist/paper-listbox-a3b7175c.js +0 -1265
  254. package/dist/pb-i18n-0611135a.js +0 -1
  255. package/dist/pb-mixin-b1caa22e.js +0 -158
  256. package/dist/polymer-hack.js +0 -1
  257. package/dist/vaadin-element-mixin-fe4a4883.js +0 -527
  258. package/lib/Control.Geocoder.min.js +0 -2
  259. package/lib/Control.Geocoder.min.js.map +0 -1
  260. package/src/assets/pb-styles.css +0 -51
  261. package/src/pb-light-dom.js +0 -41
  262. package/src/polymer-hack.js +0 -6
package/src/pb-view.js CHANGED
@@ -1,24 +1,21 @@
1
- import { LitElement, html, css } from 'lit-element';
2
- import anime from 'animejs';
3
- import { pbMixin, waitOnce } from "./pb-mixin.js";
4
- import { registry } from "./urls.js";
5
- import { translate } from "./pb-i18n.js";
6
- import { typesetMath } from "./pb-formula.js";
7
- import { loadStylesheets, themableMixin } from "./theming.js";
8
- import '@polymer/iron-ajax';
9
- import '@polymer/paper-dialog';
10
- import '@polymer/paper-dialog-scrollable';
1
+ import { LitElement, html, css } from 'lit';
2
+ import { animate } from 'animejs';
3
+ import { pbMixin, waitOnce } from './pb-mixin.js';
4
+ import { registry } from './urls.js';
5
+ import { typesetMath } from './pb-formula.js';
6
+ import { loadStylesheets, themableMixin } from './theming.js';
7
+ import './pb-fetch.js';
11
8
 
12
9
  /**
13
10
  * This is the main component for viewing text which has been transformed via an ODD.
14
11
  * The document to be viewed is determined by the `pb-document` element the property
15
12
  * `src` points to. If not overwritten, `pb-view` will use the settings defined by
16
13
  * the connected document, like view type, ODD etc.
17
- *
14
+ *
18
15
  * `pb-view` can display an entire document or just a fragment of it
19
16
  * as defined by the properties `xpath`, `xmlId` or `nodeId`. The most common use case
20
17
  * is to set `xpath` to point to a specific part of a document.
21
- *
18
+ *
22
19
  * Navigating to the next or previous fragment would usually be triggered by a separate
23
20
  * `pb-navigation` element, which sends a `pb-navigate` event to the `pb-view`. However,
24
21
  * `pb-view` also implements automatic loading of next/previous fragments if the user
@@ -50,1368 +47,1821 @@ import '@polymer/paper-dialog-scrollable';
50
47
  * @cssprop --pb-view-scroll-margin-top - Applied to any element with an id
51
48
  * @csspart content - The root div around the displayed content
52
49
  * @csspart footnotes - div containing the footnotes
53
-
50
+
54
51
  * @fires pb-start-update - Fired before the element updates its content
55
52
  * @fires pb-update - Fired when the component received content from the server
56
53
  * @fires pb-end-update - Fired after the element has finished updating its content
57
54
  * @fires pb-navigate - When received, navigate forward or backward in the document
58
- * @fires pb-zoom - When received, zoom in or out by changing font size of the content
59
55
  * @fires pb-refresh - When received, refresh the content based on the parameters passed in the event
60
56
  * @fires pb-toggle - When received, toggle content properties
61
57
  */
62
58
  export class PbView extends themableMixin(pbMixin(LitElement)) {
63
-
64
- static get properties() {
65
- return {
66
- /**
67
- * The id of a `pb-document` element this view should display.
68
- * Settings like `odd` or `view` will be taken from the `pb-document`
69
- * unless overwritten by properties in this component.
70
- *
71
- * This property is **required** and **must** point to an existing `pb-document` with
72
- * the given id.
73
- *
74
- * Setting the property after initialization will clear the properties xmlId, nodeId and odd.
75
- */
76
- src: {
77
- type: String
78
- },
79
- /**
80
- * The ODD to use for rendering the document. Overwrites an ODD defined on
81
- * `pb-document`. The odd should be specified by its name without path
82
- * or the `.odd` suffix.
83
- */
84
- odd: {
85
- type: String
86
- },
87
- /**
88
- * The view type to use for paginating the document. Either `page`, `div` or `single`.
89
- * Overwrites the same property specified on `pb-document`. Values have the following meaning:
90
- *
91
- * Value | Displayed content
92
- * ------|------------------
93
- * `page` | content is displayed page by page as determined by tei:pb
94
- * `div` | content is displayed by divisions
95
- * `single` | do not paginate but display entire content at once
96
- */
97
- view: {
98
- type: String
99
- },
100
- /**
101
- * An eXist nodeId. If specified, selects the root of the fragment of the document
102
- * which should be displayed. Normally this property is set automatically by pagination.
103
- */
104
- nodeId: {
105
- type: String,
106
- attribute: 'node-id'
107
- },
108
- /**
109
- * An xml:id to be displayed. If specified, this determines the root of the fragment to be
110
- * displayed. Use to directly navigate to a specific section.
111
- */
112
- xmlId: {
113
- type: Array,
114
- attribute: 'xml-id'
115
- },
116
- /**
117
- * An optional XPath expression: the root of the fragment to be processed is determined
118
- * by evaluating the given XPath expression. The XPath expression should be absolute.
119
- * The namespace of the document is declared as default namespace, so no prefixes should
120
- * be used.
121
- *
122
- * If the `map` property is used, it may change scope for the displayed fragment.
123
- */
124
- xpath: {
125
- type: String
126
- },
127
- /**
128
- * If defined denotes the local name of an XQuery function in `modules/map.xql`, which will be called
129
- * with the current root node and should return the node of a mapped fragment. This is helpful if one
130
- * wants, for example, to show a translation fragment aligned with the part of the transcription currently
131
- * shown. In this case, the properties of the `pb-view` would still point to the transcription, but the function
132
- * identified by map would return the corresponding fragment from the translation to be processed.
133
- *
134
- * Navigation in the document is still determined by the current root as defined through the `root`, `xpath`
135
- * and `xmlId` properties.
136
- */
137
- map: {
138
- type: String
139
- },
140
- /**
141
- * If set to true, the component will not load automatically. Instead it will wait until it receives a `pb-update`
142
- * event. Use this to make one `pb-view` component dependent on another one. Default is 'false'.
143
- */
144
- onUpdate: {
145
- type: Boolean,
146
- attribute: 'on-update'
147
- },
148
- /**
149
- * Message to display if no content was returned by the server.
150
- * Set to empty string to show nothing.
151
- */
152
- notFound: {
153
- type: String,
154
- attribute: 'not-found'
155
- },
156
- /**
157
- * The relative URL to the script on the server which will be called for loading content.
158
- */
159
- url: {
160
- type: String
161
- },
162
- /**
163
- * If set, rewrite URLs to load pages as static HTML files,
164
- * so no TEI Publisher instance is required. Use this in combination with
165
- * [tei-publisher-static](https://github.com/eeditiones/tei-publisher-static).
166
- * The value should point to the HTTP root path under which the static version
167
- * will be hosted. This is used to resolve CSS stylesheets.
168
- */
169
- static: {
170
- type: String
171
- },
172
- /**
173
- * The server returns footnotes separately. Set this property
174
- * if you wish to append them to the main text.
175
- */
176
- appendFootnotes: {
177
- type: Boolean,
178
- attribute: 'append-footnotes'
179
- },
180
- /**
181
- * Should matches be highlighted if a search has been executed?
182
- */
183
- suppressHighlight: {
184
- type: Boolean,
185
- attribute: 'suppress-highlight'
186
- },
187
- /**
188
- * CSS selector to find column breaks in the content returned
189
- * from the server. If this property is set and column breaks
190
- * are found, the component will display two columns side by side.
191
- */
192
- columnSeparator: {
193
- type: String,
194
- attribute: 'column-separator'
195
- },
196
- /**
197
- * The reading direction, i.e. 'ltr' or 'rtl'.
198
- *
199
- * @type {"ltr"|"rtl"}
200
- */
201
- direction: {
202
- type: String
203
- },
204
- /**
205
- * If set, points to an external stylesheet which should be applied to
206
- * the text *after* the ODD-generated styles.
207
- */
208
- loadCss: {
209
- type: String,
210
- attribute: 'load-css'
211
- },
212
- /**
213
- * If set, relative links (img, a) will be made absolute.
214
- */
215
- fixLinks: {
216
- type: Boolean,
217
- attribute: 'fix-links'
218
- },
219
- /**
220
- * If set, a refresh will be triggered if a `pb-i18n-update` event is received,
221
- * e.g. due to the user selecting a different interface language.
222
- *
223
- * Also requires `requireLanguage` to be set on the surrounding `pb-page`.
224
- * See there for more information.
225
- */
226
- useLanguage: {
227
- type: Boolean,
228
- attribute: 'use-language'
229
- },
230
- /**
231
- * wether to animate the view when new page is loaded. Defaults to 'false' meaning that no
232
- * animation takes place. If 'true' will apply a translateX transistion in forward/backward direction.
233
- */
234
- animation: {
235
- type: Boolean
236
- },
237
- /**
238
- * Experimental: if enabled, the view will incrementally load new document fragments if the user tries to scroll
239
- * beyond the start or end of the visible text. The feature inserts a small blank section at the top
240
- * and bottom. If this section becomes visible, a load operation will be triggered.
241
- *
242
- * Note: only browsers implementing the `IntersectionObserver` API are supported. Also the feature
243
- * does not work in two-column mode or with animations.
244
- */
245
- infiniteScroll: {
246
- type: Boolean,
247
- attribute: 'infinite-scroll'
248
- },
249
- /**
250
- * Maximum number of fragments to keep in memory if `infinite-scroll`
251
- * is enabled. If the user is scrolling beyond the maximum, fragements
252
- * will be removed from the DOM before or after the current reading position.
253
- * Default is 10. Set to zero to allow loading the entire document.
254
- */
255
- infiniteScrollMax: {
256
- type: Number,
257
- attribute: 'infinite-scroll-max'
258
- },
259
- /**
260
- * A selector pointing to other components this component depends on.
261
- * When method `wait` is called, it will wait until all referenced
262
- * components signal with a `pb-ready` event that they are ready and listening
263
- * to events.
264
- *
265
- * `pb-view` by default sets this property to select `pb-toggle-feature` and `pb-select-feature`
266
- * elements.
267
- */
268
- waitFor: {
269
- type: String,
270
- attribute: 'wait-for'
271
- },
272
- /**
273
- * By default, navigating to next/previous page will update browser parameters,
274
- * so reloading the page will load the correct position within the document.
275
- *
276
- * Set this property to disable location tracking for the component altogether.
277
- */
278
- disableHistory: {
279
- type: Boolean,
280
- attribute: 'disable-history'
281
- },
282
- /**
283
- * If set to the name of an event, the content of the pb-view will not be replaced
284
- * immediately upon updates. Instead, an event is emitted, which contains the new content
285
- * in property `root`. An event handler intercepting the event can thus modify the content.
286
- * Once it is done, it should pass the modified content to the callback function provided
287
- * in the event detail under the name `render`. See the demo for an example.
288
- */
289
- beforeUpdate: {
290
- type: String,
291
- attribute: 'before-update-event'
292
- },
293
- /**
294
- * If set, do not scroll the view to target node (e.g. given in URL hash)
295
- * after content was loaded.
296
- */
297
- noScroll: {
298
- type: Boolean,
299
- attribute: 'no-scroll'
300
- },
301
- _features: {
302
- type: Object
303
- },
304
- _content: {
305
- type: Node,
306
- attribute: false
307
- },
308
- _column1: {
309
- type: Node,
310
- attribute: false
311
- },
312
- _column2: {
313
- type: Node,
314
- attribute: false
315
- },
316
- _footnotes: {
317
- type: Node,
318
- attribute: false
319
- },
320
- _style: {
321
- type: Node,
322
- attribute: false
323
- },
324
- _additionalParams: {
325
- type: Object
326
- },
327
- ...super.properties
328
- };
59
+ static get properties() {
60
+ return {
61
+ /**
62
+ * The id of a `pb-document` element this view should display.
63
+ * Settings like `odd` or `view` will be taken from the `pb-document`
64
+ * unless overwritten by properties in this component.
65
+ *
66
+ * This property is **required** and **must** point to an existing `pb-document` with
67
+ * the given id.
68
+ *
69
+ * Setting the property after initialization will clear the properties xmlId, nodeId and odd.
70
+ */
71
+ src: {
72
+ type: String,
73
+ },
74
+ /**
75
+ * The ODD to use for rendering the document. Overwrites an ODD defined on
76
+ * `pb-document`. The odd should be specified by its name without path
77
+ * or the `.odd` suffix.
78
+ */
79
+ odd: {
80
+ type: String,
81
+ },
82
+ /**
83
+ * The view type to use for paginating the document. Either `page`, `div` or `single`.
84
+ * Overwrites the same property specified on `pb-document`. Values have the following meaning:
85
+ *
86
+ * Value | Displayed content
87
+ * ------|------------------
88
+ * `page` | content is displayed page by page as determined by tei:pb
89
+ * `div` | content is displayed by divisions
90
+ * `single` | do not paginate but display entire content at once
91
+ */
92
+ view: {
93
+ type: String,
94
+ },
95
+ /**
96
+ * Controls the pagination-by-div algorithm: if a page would have less than
97
+ * `fill` elements, it tries to fill
98
+ * up the page by pulling following divs in. When set to 0, it will never
99
+ * attempt to fill up the page. For the annotation editor this should
100
+ * always be 0.
101
+ */
102
+ fill: {
103
+ type: Number,
104
+ },
105
+ /**
106
+ * An eXist nodeId. If specified, selects the root of the fragment of the document
107
+ * which should be displayed. Normally this property is set automatically by pagination.
108
+ */
109
+ nodeId: {
110
+ type: String,
111
+ attribute: 'node-id',
112
+ },
113
+ /**
114
+ * An xml:id to be displayed. If specified, this determines the root of the fragment to be
115
+ * displayed. Use to directly navigate to a specific section.
116
+ */
117
+ xmlId: {
118
+ type: Array,
119
+ attribute: 'xml-id',
120
+ },
121
+ /**
122
+ * An optional XPath expression: the root of the fragment to be processed is determined
123
+ * by evaluating the given XPath expression. The XPath expression should be absolute.
124
+ * The namespace of the document is declared as default namespace, so no prefixes should
125
+ * be used.
126
+ *
127
+ * If the `map` property is used, it may change scope for the displayed fragment.
128
+ */
129
+ xpath: {
130
+ type: String,
131
+ },
132
+ /**
133
+ * If defined denotes the local name of an XQuery function in `modules/map.xql`, which will be called
134
+ * with the current root node and should return the node of a mapped fragment. This is helpful if one
135
+ * wants, for example, to show a translation fragment aligned with the part of the transcription currently
136
+ * shown. In this case, the properties of the `pb-view` would still point to the transcription, but the function
137
+ * identified by map would return the corresponding fragment from the translation to be processed.
138
+ *
139
+ * Navigation in the document is still determined by the current root as defined through the `root`, `xpath`
140
+ * and `xmlId` properties.
141
+ */
142
+ map: {
143
+ type: String,
144
+ },
145
+ /**
146
+ * If set to true, the component will not load automatically. Instead it will wait until it receives a `pb-update`
147
+ * event. Use this to make one `pb-view` component dependent on another one. Default is 'false'.
148
+ */
149
+ onUpdate: {
150
+ type: Boolean,
151
+ attribute: 'on-update',
152
+ },
153
+ /**
154
+ * Message to display if no content was returned by the server.
155
+ * Set to empty string to show nothing.
156
+ */
157
+ notFound: {
158
+ type: String,
159
+ attribute: 'not-found',
160
+ },
161
+ /**
162
+ * The relative URL to the script on the server which will be called for loading content.
163
+ */
164
+ url: {
165
+ type: String,
166
+ },
167
+ /**
168
+ * If set, rewrite URLs to load pages as static HTML files,
169
+ * so no TEI Publisher instance is required. Use this in combination with
170
+ * [tei-publisher-static](https://github.com/eeditiones/tei-publisher-static).
171
+ * The value should point to the HTTP root path under which the static version
172
+ * will be hosted. This is used to resolve CSS stylesheets.
173
+ */
174
+ static: {
175
+ type: String,
176
+ },
177
+ /**
178
+ * The server returns footnotes separately. Set this property
179
+ * if you wish to append them to the main text.
180
+ */
181
+ appendFootnotes: {
182
+ type: Boolean,
183
+ attribute: 'append-footnotes',
184
+ },
185
+ /**
186
+ * Should matches be highlighted if a search has been executed?
187
+ */
188
+ suppressHighlight: {
189
+ type: Boolean,
190
+ attribute: 'suppress-highlight',
191
+ },
192
+ /**
193
+ * CSS selector to find column breaks in the content returned
194
+ * from the server. If this property is set and column breaks
195
+ * are found, the component will display two columns side by side.
196
+ */
197
+ columnSeparator: {
198
+ type: String,
199
+ attribute: 'column-separator',
200
+ },
201
+ /**
202
+ * The reading direction, i.e. 'ltr' or 'rtl'.
203
+ *
204
+ * @type {"ltr"|"rtl"}
205
+ */
206
+ direction: {
207
+ type: String,
208
+ },
209
+ /**
210
+ * If set, points to an external stylesheet which should be applied to
211
+ * the text *after* the ODD-generated styles.
212
+ */
213
+ loadCss: {
214
+ type: String,
215
+ attribute: 'load-css',
216
+ },
217
+ /**
218
+ * If set, relative links (img, a) will be made absolute.
219
+ */
220
+ fixLinks: {
221
+ type: Boolean,
222
+ attribute: 'fix-links',
223
+ },
224
+ /**
225
+ * If set, a refresh will be triggered if a `pb-i18n-update` event is received,
226
+ * e.g. due to the user selecting a different interface language.
227
+ *
228
+ * Also requires `requireLanguage` to be set on the surrounding `pb-page`.
229
+ * See there for more information.
230
+ */
231
+ useLanguage: {
232
+ type: Boolean,
233
+ attribute: 'use-language',
234
+ },
235
+ /**
236
+ * wether to animate the view when new page is loaded. Defaults to 'false' meaning that no
237
+ * animation takes place. If 'true' will apply a translateX transistion in forward/backward direction.
238
+ */
239
+ animation: {
240
+ type: Boolean,
241
+ },
242
+ /**
243
+ * Experimental: if enabled, the view will incrementally load new document fragments if the user tries to scroll
244
+ * beyond the start or end of the visible text. The feature inserts a small blank section at the top
245
+ * and bottom. If this section becomes visible, a load operation will be triggered.
246
+ *
247
+ * Note: only browsers implementing the `IntersectionObserver` API are supported. Also the feature
248
+ * does not work in two-column mode or with animations.
249
+ */
250
+ infiniteScroll: {
251
+ type: Boolean,
252
+ attribute: 'infinite-scroll',
253
+ },
254
+ /**
255
+ * Maximum number of fragments to keep in memory if `infinite-scroll`
256
+ * is enabled. If the user is scrolling beyond the maximum, fragements
257
+ * will be removed from the DOM before or after the current reading position.
258
+ * Default is 10. Set to zero to allow loading the entire document.
259
+ */
260
+ infiniteScrollMax: {
261
+ type: Number,
262
+ attribute: 'infinite-scroll-max',
263
+ },
264
+ /**
265
+ * A selector pointing to other components this component depends on.
266
+ * When method `wait` is called, it will wait until all referenced
267
+ * components signal with a `pb-ready` event that they are ready and listening
268
+ * to events.
269
+ *
270
+ * `pb-view` by default sets this property to select `pb-toggle-feature` and `pb-select-feature`
271
+ * elements.
272
+ */
273
+ waitFor: {
274
+ type: String,
275
+ attribute: 'wait-for',
276
+ },
277
+ /**
278
+ * By default, navigating to next/previous page will update browser parameters,
279
+ * so reloading the page will load the correct position within the document.
280
+ *
281
+ * Set this property to disable location tracking for the component altogether.
282
+ */
283
+ disableHistory: {
284
+ type: Boolean,
285
+ attribute: 'disable-history',
286
+ },
287
+ /**
288
+ * If set to true, pb-view will only read from the registry and never write to it.
289
+ * This is useful when pb-view is used as a simple receiver of navigation updates
290
+ * from other components (e.g., pb-tify). When enabled, pb-view will:
291
+ * - Still subscribe to registry changes
292
+ * - Still read from registry to determine what to display
293
+ * - Never call registry.replace or registry.commit after initial connection
294
+ *
295
+ * This prevents pb-view from resetting the registry with stale state during navigation,
296
+ * which can cause bounce-back issues when used with components like pb-tify.
297
+ *
298
+ * Default is false (pb-view can write to registry as before).
299
+ */
300
+ readOnlyRegistry: {
301
+ type: Boolean,
302
+ attribute: 'read-only-registry',
303
+ },
304
+ /**
305
+ * If set to the name of an event, the content of the pb-view will not be replaced
306
+ * immediately upon updates. Instead, an event is emitted, which contains the new content
307
+ * in property `root`. An event handler intercepting the event can thus modify the content.
308
+ * Once it is done, it should pass the modified content to the callback function provided
309
+ * in the event detail under the name `render`. See the demo for an example.
310
+ */
311
+ beforeUpdate: {
312
+ type: String,
313
+ attribute: 'before-update-event',
314
+ },
315
+ /**
316
+ * If set, do not scroll the view to target node (e.g. given in URL hash)
317
+ * after content was loaded.
318
+ */
319
+ noScroll: {
320
+ type: Boolean,
321
+ attribute: 'no-scroll',
322
+ },
323
+ _features: {
324
+ type: Object,
325
+ },
326
+ _content: {
327
+ type: Node,
328
+ attribute: false,
329
+ },
330
+ _column1: {
331
+ type: Node,
332
+ attribute: false,
333
+ },
334
+ _column2: {
335
+ type: Node,
336
+ attribute: false,
337
+ },
338
+ _footnotes: {
339
+ type: Node,
340
+ attribute: false,
341
+ },
342
+ _style: {
343
+ type: Node,
344
+ attribute: false,
345
+ },
346
+ _additionalParams: {
347
+ type: Object,
348
+ },
349
+ ...super.properties,
350
+ };
351
+ }
352
+
353
+ constructor() {
354
+ super();
355
+ this.src = null;
356
+ this.url = null;
357
+ this.readOnlyRegistry = false; // Default: pb-view can write to registry
358
+ this._registryInitialized = false; // Track if registry has been initialized
359
+ this.onUpdate = false;
360
+ this.appendFootnotes = false;
361
+ this.notFound = null;
362
+ this.animation = false;
363
+ this.direction = 'ltr';
364
+ this.suppressHighlight = false;
365
+ this.highlight = false;
366
+ this.infiniteScrollMax = 10;
367
+ this.disableHistory = false;
368
+ this.beforeUpdate = null;
369
+ this.noScroll = false;
370
+ this._features = {};
371
+ this._additionalParams = {};
372
+ this._selector = {};
373
+ this._chunks = [];
374
+ this._scrollTarget = null;
375
+ this._loading = false;
376
+ this._lastRequestKey = null;
377
+ this.static = null;
378
+ // Debouncing for refresh calls (Option 6: Hybrid approach)
379
+ this._refreshDebounceTimer = null;
380
+ this._pendingRefreshEvent = null;
381
+ this._hasLoadedOnce = false; // Track if view has loaded at least once
382
+ this._lastLoadedId = null; // Track the last id that was actually loaded (for detecting navigation)
383
+ }
384
+
385
+ attributeChangedCallback(name, oldVal, newVal) {
386
+ super.attributeChangedCallback(name, oldVal, newVal);
387
+ switch (name) {
388
+ case 'src':
389
+ this._updateSource(newVal, oldVal);
390
+ break;
329
391
  }
392
+ }
330
393
 
331
- constructor() {
332
- super();
333
- this.src = null;
334
- this.url = null;
335
- this.onUpdate = false;
336
- this.appendFootnotes = false;
337
- this.notFound = null;
338
- this.animation = false;
339
- this.direction = 'ltr';
340
- this.suppressHighlight = false;
341
- this.highlight = false;
342
- this.infiniteScrollMax = 10;
343
- this.disableHistory = false;
344
- this.beforeUpdate = null;
345
- this.noScroll = false;
346
- this._features = {};
347
- this._additionalParams = {};
348
- this._selector = {};
349
- this._chunks = [];
350
- this._scrollTarget = null;
351
- this.static = null;
352
- }
394
+ connectedCallback() {
395
+ super.connectedCallback();
353
396
 
354
- attributeChangedCallback(name, oldVal, newVal) {
355
- super.attributeChangedCallback(name, oldVal, newVal);
356
- switch (name) {
357
- case 'src':
358
- this._updateSource(newVal, oldVal);
359
- break;
360
- }
397
+ if (this.loadCss) {
398
+ waitOnce('pb-page-ready', () => {
399
+ loadStylesheets([this.toAbsoluteURL(this.loadCss)]).then(theme => {
400
+ if (theme) {
401
+ this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, theme];
402
+ }
403
+ });
404
+ });
361
405
  }
362
406
 
363
- connectedCallback() {
364
- super.connectedCallback();
365
-
366
- if (this.loadCss) {
367
- waitOnce('pb-page-ready', () => {
368
- loadStylesheets([this.toAbsoluteURL(this.loadCss)])
369
- .then((theme) => {
370
- this.shadowRoot.adoptedStyleSheets = [...this.shadowRoot.adoptedStyleSheets, theme];
371
- });
372
- });
373
- }
374
-
375
- if (this.infiniteScroll) {
376
- this.columnSeparator = null;
377
- this.animation = false;
378
- this._content = document.createElement('div');
379
- this._content.className = 'infinite-content';
380
- }
381
-
382
- if (!this.disableHistory) {
383
- if (registry.state.id && !this.xmlId) {
384
- this.xmlId = registry.state.id;
385
- }
386
-
387
- if (registry.state.action && registry.state.action === 'search') {
388
- this.highlight = true;
389
- }
390
-
391
- if (this.view === 'single') {
392
- this.nodeId = null;
393
- } else if (registry.state.root && !this.nodeId) {
394
- this.nodeId = registry.state.root;
395
- }
396
-
397
- const newState = {
398
- id: this.xmlId,
399
- view: this.getView(),
400
- odd: this.getOdd(),
401
- path: this.getDocument().path
402
- };
403
- if (this.view !== 'single') {
404
- newState.root = this.nodeId;
405
- }
406
- console.log('id: %s; state: %o', this.id, newState);
407
- registry.replace(this, newState);
408
-
409
- registry.subscribe(this, (state) => {
410
- this._setState(state);
411
- this._refresh();
412
- });
413
- }
414
- if (!this.waitFor) {
415
- this.waitFor = 'pb-toggle-feature,pb-select-feature,pb-navigation';
416
- }
407
+ if (this.infiniteScroll) {
408
+ this.columnSeparator = null;
409
+ this.animation = false;
410
+ this._content = document.createElement('div');
411
+ this._content.className = 'infinite-content';
412
+ }
417
413
 
418
- this.subscribeTo('pb-navigate', ev => {
419
- if (ev.detail.source && ev.detail.source === this) {
420
- return;
421
- }
422
- this.navigate(ev.detail.direction);
423
- });
424
- this.subscribeTo('pb-refresh', this._refresh.bind(this));
425
- this.subscribeTo('pb-toggle', ev => {
426
- this.toggleFeature(ev);
414
+ if (!this.disableHistory) {
415
+ if (registry.state.id && !this.xmlId) {
416
+ this.xmlId = registry.state.id;
417
+ }
418
+
419
+ if (registry.state.action && registry.state.action === 'search') {
420
+ this.highlight = true;
421
+ }
422
+
423
+ if (this.view === 'single') {
424
+ this.nodeId = null;
425
+ } else if (registry.state.root && !this.nodeId) {
426
+ this.nodeId = registry.state.root;
427
+ }
428
+
429
+ // Only call registry.replace during initial connection, not during navigation
430
+ // If readOnlyRegistry is true, never call registry.replace (even during initial connection)
431
+ // This prevents pb-view from resetting registry with stale state during navigation
432
+ // Check both property (LitElement) and attribute (XML/XHTML compatibility)
433
+ const isReadOnly = this.readOnlyRegistry || this.hasAttribute('read-only-registry');
434
+ if (!this._registryInitialized && !isReadOnly) {
435
+ const _doc = this.getDocument ? this.getDocument() : null;
436
+ const newState = {
437
+ id: this.xmlId,
438
+ view: this.getView(),
439
+ odd: this.getOdd(),
440
+ path: _doc ? _doc.path : undefined,
441
+ };
442
+ if (this.view !== 'single') {
443
+ newState.root = this.nodeId;
444
+ }
445
+ if (this.fill) {
446
+ newState.fill = this.fill;
447
+ }
448
+ console.warn('[pb-view] connectedCallback: Calling registry.replace (read-only-registry not set)', {
449
+ readOnlyRegistry: this.readOnlyRegistry,
450
+ hasAttribute: this.hasAttribute('read-only-registry'),
451
+ isReadOnly,
452
+ _registryInitialized: this._registryInitialized,
453
+ newState
427
454
  });
428
- this.subscribeTo('pb-zoom', ev => {
429
- this.zoom(ev.detail.direction);
455
+ registry.replace(this, newState);
456
+ this._registryInitialized = true;
457
+ } else if (isReadOnly) {
458
+ console.log('[pb-view] connectedCallback: Skipping registry.replace (read-only-registry is set)', {
459
+ readOnlyRegistry: this.readOnlyRegistry,
460
+ hasAttribute: this.hasAttribute('read-only-registry'),
461
+ isReadOnly,
462
+ _registryInitialized: this._registryInitialized
430
463
  });
431
- this.subscribeTo('pb-i18n-update', ev => {
432
- const needsRefresh = this._features.language && this._features.language !== ev.detail.language;
433
- this._features.language = ev.detail.language;
434
- if (this.useLanguage && needsRefresh) {
435
- this._setState(registry.getState(this));
436
- this._refresh();
437
- }
438
- }, []);
439
-
440
- this.signalReady();
464
+ }
441
465
 
442
- if (this.onUpdate) {
443
- this.subscribeTo('pb-update', (ev) => {
444
- this._refresh(ev);
445
- });
446
- }
466
+ registry.subscribe(this, state => {
467
+ this._setState(state);
468
+ // Use debounced refresh to batch rapid registry changes
469
+ this._refresh(null); // Pass null as event, registry state will be used
470
+ });
471
+ }
472
+ if (!this.waitFor) {
473
+ this.waitFor = 'pb-toggle-feature,pb-select-feature,pb-navigation';
447
474
  }
448
475
 
449
- disconnectedCallback() {
450
- super.disconnectedCallback();
451
- if (this._scrollObserver) {
452
- this._scrollObserver.disconnect();
453
- }
476
+ this.subscribeTo('pb-navigate', ev => {
477
+ if (ev.detail.source && ev.detail.source === this) {
478
+ return;
479
+ }
480
+ this.navigate(ev.detail.direction);
481
+ });
482
+
483
+ this.subscribeTo('pb-toggle', ev => {
484
+ this.toggleFeature(ev);
485
+ });
486
+ this.subscribeTo(
487
+ 'pb-i18n-update',
488
+ ev => {
489
+ const needsRefresh =
490
+ this._features.language && this._features.language !== ev.detail.language;
491
+ this._features.language = ev.detail.language;
492
+ if (this.useLanguage && needsRefresh) {
493
+ this._setState(registry.getState(this));
494
+ this._refresh();
495
+ }
496
+ },
497
+ [],
498
+ );
499
+
500
+ this.signalReady();
501
+
502
+ if (this.onUpdate) {
503
+ this.subscribeTo('pb-update', ev => {
504
+ this._refresh(ev);
505
+ });
454
506
  }
507
+ }
455
508
 
456
- firstUpdated() {
457
- super.firstUpdated();
458
- this.enableScrollbar(true);
459
- if (this.infiniteScroll) {
460
- this._topObserver = this.shadowRoot.getElementById('top-observer');
461
- this._bottomObserver = this.shadowRoot.getElementById('bottom-observer');
462
- this._bottomObserver.style.display = 'none';
463
- this._topObserver.style.display = 'none';
464
- this._scrollObserver = new IntersectionObserver((entries) => {
465
- if (!this._content) {
466
- return;
509
+ disconnectedCallback() {
510
+ super.disconnectedCallback();
511
+ if (this._scrollObserver) {
512
+ this._scrollObserver.disconnect();
513
+ }
514
+ // Reset registry initialization flag when component is disconnected
515
+ // BUT: If read-only-registry is set, don't reset - we never want to call registry.replace
516
+ // This prevents pb-view from resetting registry when pb-grid rebuilds DOM
517
+ const isReadOnly = this.readOnlyRegistry || this.hasAttribute('read-only-registry');
518
+ if (!isReadOnly) {
519
+ this._registryInitialized = false;
520
+ }
521
+ }
522
+
523
+ firstUpdated() {
524
+ super.firstUpdated();
525
+ this.enableScrollbar(true);
526
+ if (this.infiniteScroll) {
527
+ this._topObserver = this.shadowRoot.getElementById('top-observer');
528
+ this._bottomObserver = this.shadowRoot.getElementById('bottom-observer');
529
+ this._bottomObserver.style.display = 'none';
530
+ this._topObserver.style.display = 'none';
531
+ this._scrollObserver = new IntersectionObserver(entries => {
532
+ if (!this._content) {
533
+ return;
534
+ }
535
+ entries.forEach(entry => {
536
+ if (entry.isIntersecting) {
537
+ if (entry.target.id === 'bottom-observer') {
538
+ const lastChild = this._content.lastElementChild;
539
+ if (lastChild) {
540
+ const next = lastChild.getAttribute('data-next');
541
+ if (next && !this._content.querySelector(`[data-root="${next}"]`)) {
542
+ this._checkChunks('forward');
543
+ this._load(next, 'forward');
467
544
  }
468
- entries.forEach((entry) => {
469
- if (entry.isIntersecting) {
470
- if (entry.target.id === 'bottom-observer') {
471
- const lastChild = this._content.lastElementChild;
472
- if (lastChild) {
473
- const next = lastChild.getAttribute('data-next');
474
- if (next && !this._content.querySelector(`[data-root="${next}"]`)) {
475
- console.log('<pb-view> Loading next page: %s', next);
476
- this._checkChunks('forward');
477
- this._load(next, 'forward');
478
- }
479
- }
480
- } else {
481
- const firstChild = this._content.firstElementChild;
482
- if (firstChild) {
483
- const previous = firstChild.getAttribute('data-previous');
484
- if (previous && !this._content.querySelector(`[data-root="${previous}"]`)) {
485
- this._checkChunks('backward');
486
- this._load(previous, 'backward');
487
- }
488
- }
489
- }
490
- }
491
- });
492
- });
493
- }
494
- if (!this.onUpdate) {
495
- waitOnce('pb-page-ready', (data) => {
496
- if (data && data.language) {
497
- this._features.language = data.language;
545
+ }
546
+ } else {
547
+ const firstChild = this._content.firstElementChild;
548
+ if (firstChild) {
549
+ const previous = firstChild.getAttribute('data-previous');
550
+ if (previous && !this._content.querySelector(`[data-root="${previous}"]`)) {
551
+ this._checkChunks('backward');
552
+ this._load(previous, 'backward');
498
553
  }
499
- this.wait(() => {
500
- if (!this.disableHistory) {
501
- this._setState(registry.state);
502
- }
503
- this._refresh();
504
- });
505
- });
506
- }
554
+ }
555
+ }
556
+ }
557
+ });
558
+ });
507
559
  }
508
-
509
- /**
510
- * Returns the ODD used to render content.
511
- *
512
- * @returns the ODD being used
513
- */
514
- getOdd() {
515
- return this.odd || this.getDocument().odd || "teipublisher";
560
+ if (!this.onUpdate) {
561
+ waitOnce('pb-page-ready', data => {
562
+ if (data && data.language) {
563
+ this._features.language = data.language;
564
+ }
565
+ this.wait(() => {
566
+ if (!this.disableHistory) {
567
+ this._setState(registry.state);
568
+ }
569
+ this._refresh();
570
+ });
571
+ });
516
572
  }
517
-
518
- getView() {
519
- return this.view || this.getDocument().view || "single";
573
+ // Subscribe to pb-refresh events
574
+ // Application code should specify subscribe="transcription" (or other channel) via HTML attribute
575
+ // If no subscribe attribute is set, uses defaultChannel (following pb-mixin pattern)
576
+ // This makes components generic and reusable - applications configure the channel
577
+ this.subscribeTo('pb-refresh', (ev) => {
578
+ this._refresh(ev);
579
+ });
580
+ }
581
+
582
+ /**
583
+ * Returns the ODD used to render content.
584
+ *
585
+ * @returns the ODD being used
586
+ */
587
+ getOdd() {
588
+ try {
589
+ return this.odd || this.getDocument().odd || 'teipublisher';
590
+ } catch {
591
+ return this.odd || 'teipublisher';
520
592
  }
593
+ }
521
594
 
522
- /**
523
- * Trigger an update of this element's content
524
- */
525
- forceUpdate() {
526
- this._load(this.nodeId);
527
-
595
+ getView() {
596
+ try {
597
+ return this.view || this.getDocument().view || 'single';
598
+ } catch {
599
+ return this.view || 'single';
528
600
  }
529
-
530
- animate() {
531
- // animate new element if 'animation' property is 'true'
532
- if (this.animation) {
533
- if (this.lastDirection === 'forward') {
534
- anime({
535
- targets: this.shadowRoot.getElementById('view'),
536
- opacity: [0, 1],
537
- translateX: [1000, 0],
538
- duration: 300,
539
- easing: 'linear'
540
- });
541
- } else {
542
- anime({
543
- targets: this.shadowRoot.getElementById('view'),
544
- opacity: [0, 1],
545
- translateX: [-1000, 0],
546
- duration: 300,
547
- easing: 'linear'
548
- });
549
- }
550
- }
601
+ }
602
+
603
+ /**
604
+ * Trigger an update of this element's content
605
+ */
606
+ forceUpdate() {
607
+ this._load(this.nodeId);
608
+ }
609
+
610
+ animate() {
611
+ // animate new element if 'animation' property is 'true'
612
+ if (this.animation) {
613
+ if (this.lastDirection === 'forward') {
614
+ animate(this.shadowRoot.getElementById('view'), {
615
+ opacity: [0, 1],
616
+ translateX: [1000, 0],
617
+ duration: 300,
618
+ ease: 'linear',
619
+ });
620
+ } else {
621
+ animate(this.shadowRoot.getElementById('view'), {
622
+ opacity: [0, 1],
623
+ translateX: [-1000, 0],
624
+ duration: 300,
625
+ ease: 'linear',
626
+ });
627
+ }
551
628
  }
629
+ }
552
630
 
553
- enableScrollbar(enable) {
554
- if (enable) {
555
- this.classList.add('noscroll');
556
- } else {
557
- this.classList.remove('noscroll');
558
- }
631
+ enableScrollbar(enable) {
632
+ if (enable) {
633
+ this.classList.add('noscroll');
634
+ } else {
635
+ this.classList.remove('noscroll');
559
636
  }
560
-
561
- _refresh(ev) {
562
- if (ev && ev.detail) {
563
- if (ev.detail.hash && !this.noScroll && !(ev.detail.id || ev.detail.path || ev.detail.odd || ev.detail.view || ev.detail.position)) {
564
- // if only the scroll target has changed: scroll to the element without reloading
565
- this._scrollTarget = ev.detail.hash;
566
- const target = this.shadowRoot.getElementById(this._scrollTarget);
567
- if (target) {
568
- setTimeout(() => target.scrollIntoView({block: 'nearest'}));
569
- }
570
- return;
571
- }
572
- if (ev.detail.path) {
573
- const doc = this.getDocument();
574
- doc.path = ev.detail.path;
575
- }
576
- if (ev.detail.id) {
577
- this.xmlId = ev.detail.id;
578
- } else if (ev.detail.id == null) {
579
- this.xmlId = null;
580
- }
581
- this.odd = ev.detail.odd || this.odd;
582
- if (ev.detail.columnSeparator !== undefined) {
583
- this.columnSeparator = ev.detail.columnSeparator;
584
- }
585
- this.view = ev.detail.view || this.getView();
586
- if (ev.detail.xpath) {
587
- this.xpath = ev.detail.xpath;
588
- this.nodeId = null;
589
- }
590
- // clear nodeId if set to null
591
- if (ev.detail.root === null) {
592
- this.nodeId = null;
593
- } else {
594
- this.nodeId = (ev.detail.position !== undefined ? ev.detail.position : ev.detail.root) || this.nodeId;
595
- }
596
-
597
- // check if the URL template needs any other parameters
598
- // and set them on this._additionalParams
599
- registry.pathParams.forEach((key) => {
600
- this._additionalParams[key] = ev.detail[key];
601
- });
602
-
603
- if (!this.noScroll) {
604
- this._scrollTarget = ev.detail.hash;
605
- }
606
- }
607
- this._updateStyles();
608
- if (this.infiniteScroll) {
609
- this._clear();
610
- }
611
- this._load(this.nodeId);
637
+ }
638
+
639
+ _refresh(ev) {
640
+ // Store the pending event for debouncing
641
+ this._pendingRefreshEvent = ev;
642
+
643
+ // Clear existing debounce timer
644
+ if (this._refreshDebounceTimer) {
645
+ clearTimeout(this._refreshDebounceTimer);
612
646
  }
613
-
614
- _load(pos, direction) {
615
- const doc = this.getDocument();
616
-
617
- if (!doc.path) {
618
- console.log("No path");
619
- return;
620
- }
621
-
622
- if (this._loading) {
623
- return;
624
- }
625
- this._loading = true;
626
- const params = this.getParameters(pos);
627
- if (direction) {
628
- params._dir = direction;
629
- }
630
- // this.$.view.style.opacity=0;
631
-
632
- this._doLoad(params);
647
+
648
+ // Debounce: wait 150ms to batch rapid changes
649
+ this._refreshDebounceTimer = setTimeout(() => {
650
+ this._doRefresh(this._pendingRefreshEvent);
651
+ this._pendingRefreshEvent = null;
652
+ this._refreshDebounceTimer = null;
653
+ }, 150);
654
+ }
655
+
656
+ _doRefresh(ev) {
657
+ // Merge registry state with event details, prioritizing registry state
658
+ const registryState = registry.getState(this);
659
+ const eventDetail = ev && ev.detail ? ev.detail : {};
660
+
661
+ // Priority: registry state > event detail > current values
662
+ const mergedState = {
663
+ ...eventDetail,
664
+ ...registryState, // Registry state overrides event detail
665
+ };
666
+
667
+ // Check if this is a metadata panel BEFORE processing any state changes
668
+ // Check pb-param elements for mode parameter
669
+ let modeParam = null;
670
+ const modeParamEl = this.querySelector && this.querySelector('pb-param[name="mode"]');
671
+ if (modeParamEl) {
672
+ modeParam = modeParamEl.getAttribute('value');
673
+ }
674
+ // Also check _additionalParams (might be set from registry or events)
675
+ if (!modeParam && this._additionalParams) {
676
+ modeParam = this._additionalParams.mode || this._additionalParams['user.mode'];
677
+ }
678
+
679
+ const isMetadataPanel = modeParam === 'metadata-panel' ||
680
+ (this.xpath && this.view === 'single' && !this.nodeId);
681
+
682
+ // Skip refresh if this view has xpath (e.g., metadata panel) - it shouldn't react to page navigation
683
+ // BUT allow initial load (when _hasLoadedOnce is false)
684
+ if (this.xpath && !mergedState.xpath && this._hasLoadedOnce) {
685
+ // This view is bound to a specific xpath (like metadata), don't change it after initial load
686
+ return;
687
+ }
688
+
689
+ // Skip refresh for metadata panel - it shouldn't react to page navigation
690
+ // BUT allow initial load (when _hasLoadedOnce is false)
691
+ if (isMetadataPanel && this._hasLoadedOnce) {
692
+ // Metadata panel shouldn't react to page navigation events (root/id changes)
693
+ // Only allow updates if explicitly changing xpath or view mode
694
+ // Filter out canvas IDs before checking
695
+ const hasCanvasId = mergedState.id && /\.jpg$|_\d{2,3}\.jpg/.test(String(mergedState.id));
696
+ if (mergedState.root || (mergedState.id && !hasCanvasId) || mergedState.position) {
697
+ // This is a page navigation event - skip it for metadata panel
698
+ return;
699
+ }
700
+ }
701
+
702
+ // For metadata panel, filter out canvas IDs from merged state
703
+ // Canvas IDs (like "A-N-38_004.jpg") should not affect metadata panel content
704
+ // Create a filtered copy if needed
705
+ let filteredState = mergedState;
706
+ if (isMetadataPanel && mergedState.id) {
707
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(mergedState.id));
708
+ if (looksLikeCanvasId) {
709
+ // Remove canvas ID from merged state for metadata panel
710
+ filteredState = { ...mergedState };
711
+ delete filteredState.id;
712
+ }
713
+ }
714
+
715
+ // Use filteredState for the rest of the function (filteredState === mergedState if no filtering was needed)
716
+ const stateToUse = filteredState;
717
+
718
+ // Handle hash-only changes (scroll without reload)
719
+ if (stateToUse.hash &&
720
+ !this.noScroll &&
721
+ !(stateToUse.id || stateToUse.path || stateToUse.odd || stateToUse.view || stateToUse.position)) {
722
+ this._scrollTarget = stateToUse.hash;
723
+ const target = this.shadowRoot.getElementById(this._scrollTarget);
724
+ if (target) {
725
+ setTimeout(() => target.scrollIntoView({ block: 'nearest' }));
726
+ }
727
+ return;
728
+ }
729
+
730
+ // Store old values BEFORE updating - critical for change detection
731
+ // IMPORTANT: For _refresh(null) case where _setState was called first,
732
+ // _setState may have already updated this.xmlId, so oldXmlId might be the new value.
733
+ // We'll compare against _lastLoadedId (what was actually loaded) to detect changes.
734
+ const oldXmlId = this.xmlId;
735
+ const oldNodeId = this.nodeId;
736
+ const oldPath = this.getDocument ? (this.getDocument()?.path || null) : null;
737
+ // For _refresh(null) case, also check if _setState set an ID in _features or _additionalParams
738
+ // that's different from what was loaded
739
+ // CRITICAL: Check these BEFORE we process the ID, because processing might update them
740
+ const additionalParamsIdBefore = this._additionalParams && this._additionalParams.id ? this._additionalParams.id : null;
741
+ const featuresIdBefore = this._features && this._features.id ? this._features.id : null;
742
+ const lastLoadedIdForComparison = this._lastLoadedId; // What was actually loaded via API
743
+
744
+ // For _refresh(null) case: if _setState set an ID that's different from what was loaded,
745
+ // we should reload even if oldXmlId === newId (because _setState already updated this.xmlId)
746
+ const setIdChanged = (additionalParamsIdBefore && additionalParamsIdBefore !== lastLoadedIdForComparison && lastLoadedIdForComparison !== null) ||
747
+ (featuresIdBefore && featuresIdBefore !== lastLoadedIdForComparison && lastLoadedIdForComparison !== null);
748
+
749
+ // Apply merged state
750
+ // CRITICAL: Event detail takes precedence over registry state for path/odd changes
751
+ // This is important because events explicitly request path/odd changes, while registry might have old values
752
+ const eventPath = ev && ev.detail && ev.detail.path ? ev.detail.path : null;
753
+ const eventOdd = ev && ev.detail && ev.detail.odd ? ev.detail.odd : null;
754
+ // Use event path/odd if present, otherwise use stateToUse (registry state)
755
+ const pathToApply = eventPath || stateToUse.path;
756
+ const oddToApply = eventOdd || stateToUse.odd;
757
+
758
+ if (pathToApply) {
759
+ const doc = this.getDocument();
760
+ if (doc) {
761
+ doc.path = pathToApply;
762
+ }
763
+ }
764
+
765
+ // Store for later use in pathChanged/oddChanged checks
766
+ const newPath = pathToApply;
767
+ const newOdd = oddToApply;
768
+
769
+ // Handle id parameter - distinguish between XML IDs and canvas IDs
770
+ // Canvas IDs (from pb-tify) have patterns like "A-N-38_004.jpg" or end with ".jpg"
771
+ // XML IDs are actual xml:id attributes in the TEI document
772
+ // CRITICAL: Use ev.detail.id directly to avoid issues with filtered/overridden state
773
+ // Also check _additionalParams and _features for ID (set via _setState when ev is null)
774
+ const eventId = ev && ev.detail && ev.detail.id ? ev.detail.id : null;
775
+ const additionalParamsId = this._additionalParams && this._additionalParams.id ? this._additionalParams.id : null;
776
+ const featuresId = this._features && this._features.id ? this._features.id : null;
777
+ // For _refresh(null) case, also check if _setState was called with id
778
+ // _setState stores id in _features if not in pathParams, or _additionalParams if in pathParams
779
+ const idToProcess = eventId || (stateToUse.id !== undefined ? stateToUse.id : null) || additionalParamsId || featuresId;
780
+
781
+ if (idToProcess) {
782
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(idToProcess));
783
+ if (!looksLikeCanvasId) {
784
+ // It's a real XML ID, use it
785
+ this.xmlId = idToProcess;
786
+ } else {
787
+ // If it's a canvas ID, store it in _additionalParams so getParameters can use it
788
+ // This ensures the request includes the correct canvas ID even though we don't set xmlId
789
+ this._additionalParams = this._additionalParams || {};
790
+ this._additionalParams.id = idToProcess;
791
+ }
792
+ // If it's a canvas ID, don't set xmlId - use root/position for navigation instead
793
+ }
794
+
795
+ // Use event odd if present, otherwise use stateToUse.odd, otherwise keep current
796
+ this.odd = oddToApply || this.odd;
797
+
798
+ if (stateToUse.columnSeparator !== undefined) {
799
+ this.columnSeparator = stateToUse.columnSeparator;
800
+ }
801
+
802
+ this.view = stateToUse.view || this.getView();
803
+ this.fill = stateToUse.fill || this.fill;
804
+
805
+ if (stateToUse.xpath) {
806
+ this.xpath = stateToUse.xpath;
807
+ this.nodeId = null;
808
+ }
809
+
810
+ // Handle root/position (prioritize registry state)
811
+
812
+ let newNodeId = this.nodeId;
813
+ if (stateToUse.root === null) {
814
+ newNodeId = null;
815
+ } else {
816
+ newNodeId = (stateToUse.position !== undefined ? stateToUse.position : stateToUse.root) || this.nodeId;
817
+ }
818
+
819
+ // Check if nodeId actually changed - if not, skip loading to avoid unnecessary requests
820
+ const nodeIdChanged = newNodeId !== oldNodeId;
821
+
822
+ // Also check if id changed - even if nodeId (root) is the same, id change means different page
823
+ // Use ev.detail.id directly (from the event) to avoid issues with filtered/overridden state
824
+ // Fall back to stateToUse.id, _additionalParams.id, _features.id, or this.xmlId if event doesn't have id
825
+ // CRITICAL: For _refresh(null) case, check if _setState set an ID that's different from oldXmlId
826
+ const eventIdForChangeCheck = ev && ev.detail && ev.detail.id ? ev.detail.id : null;
827
+ const additionalParamsIdForCheck = this._additionalParams && this._additionalParams.id ? this._additionalParams.id : null;
828
+ const featuresIdForCheck = this._features && this._features.id ? this._features.id : null;
829
+ const newId = eventIdForChangeCheck || (stateToUse.id !== undefined ? stateToUse.id : null) || additionalParamsIdForCheck || featuresIdForCheck || this.xmlId;
830
+ // Check if ID changed: compare against oldXmlId OR lastLoadedIdForComparison (what was actually loaded)
831
+ // For _refresh(null) case where _setState was called first, _setState already updated this.xmlId,
832
+ // so oldXmlId might be the new value. Compare against lastLoadedIdForComparison to detect changes.
833
+ // Also check if _setState set an ID in _features/_additionalParams that's different from what was loaded
834
+ // Check if ID changed: compare against oldXmlId OR lastLoadedIdForComparison (what was actually loaded)
835
+ // For _refresh(null) case where _setState was called first, _setState already updated this.xmlId,
836
+ // so oldXmlId might be the new value. Compare against lastLoadedIdForComparison to detect changes.
837
+ // Also check if _setState set an ID in _features/_additionalParams that's different from what was loaded
838
+ // CRITICAL: If _setState set a new ID (setIdChanged is true), always reload regardless of other conditions
839
+ // Also reload if we have a new ID but _lastLoadedId is null (first load or ID was set externally)
840
+ const idChanged = newId && (
841
+ newId !== oldXmlId ||
842
+ (newId !== lastLoadedIdForComparison && lastLoadedIdForComparison !== null) ||
843
+ setIdChanged || // _setState set a new ID - always reload
844
+ (lastLoadedIdForComparison === null && newId) // First load with ID, or ID set externally
845
+ );
846
+
847
+ // Check if path changed - path change means different document, should always reload
848
+ // oldPath was calculated above before updating doc.path
849
+ // newPath was calculated above (event path takes precedence over registry state)
850
+ const pathChanged = newPath && newPath !== oldPath;
851
+
852
+ // Check if odd changed - odd change means different document format, should reload
853
+ // newOdd was calculated above (event odd takes precedence over registry state)
854
+ const oldOdd = this.odd;
855
+ const oddChanged = newOdd && newOdd !== oldOdd;
856
+
857
+ // CRITICAL: If we received a pb-refresh event, check if it's for a different page
858
+ // This ensures we reload when navigation occurs, even if registry already updated our state
859
+ const eventHasId = ev && ev.detail && ev.detail.id;
860
+ // _lastLoadedId tracks what we actually loaded via API, not what registry set
861
+ // If null, we haven't loaded anything yet, so we should load
862
+ const lastLoadedId = this._lastLoadedId;
863
+ // If we received an event with an id different from what we last loaded, reload
864
+ // OR if we haven't loaded anything yet (_lastLoadedId is null), reload
865
+ // Also check against oldXmlId as fallback (in case _lastLoadedId wasn't set)
866
+ const shouldReloadFromEvent = eventHasId && (
867
+ lastLoadedId === null ||
868
+ ev.detail.id !== lastLoadedId ||
869
+ (oldXmlId && ev.detail.id !== oldXmlId)
870
+ );
871
+
872
+ const willSkip = !nodeIdChanged && !idChanged && !pathChanged && !oddChanged && !shouldReloadFromEvent && this._hasLoadedOnce;
873
+
874
+ if (willSkip) {
875
+ // Neither nodeId nor id changed and event id hasn't changed - skip loading
876
+ // This prevents unnecessary /api/parts/ requests when navigation doesn't actually change the content
877
+
878
+ // Still update other properties that might have changed
879
+ registry.pathParams.forEach(key => {
880
+ if (stateToUse[key] !== undefined) {
881
+ this._additionalParams[key] = stateToUse[key];
882
+ }
883
+ });
884
+
885
+ // Update xmlId if it changed (even if we're not reloading)
886
+ // this.xmlId was already updated above if stateToUse.id was set
887
+
888
+ if (!this.noScroll) {
889
+ this._scrollTarget = stateToUse.hash;
890
+ }
891
+
892
+ this._updateStyles();
893
+ return;
894
+ }
895
+
896
+ // NodeId changed or id changed or first load - update and load
897
+ this.nodeId = newNodeId;
898
+
899
+ // xmlId was already updated above if stateToUse.id was set and not a canvas ID
900
+
901
+ // Track the id we're about to load so we can detect if a pb-refresh event
902
+ // is for a different page than what we last loaded
903
+ const idToLoad = mergedState.id || this.xmlId;
904
+
905
+ // Check if the URL template needs any other parameters
906
+ // For metadata panel, canvas IDs are already filtered out in filteredState
907
+ registry.pathParams.forEach(key => {
908
+ if (stateToUse[key] !== undefined) {
909
+ this._additionalParams[key] = stateToUse[key];
910
+ }
911
+ });
912
+
913
+ if (!this.noScroll) {
914
+ this._scrollTarget = stateToUse.hash;
915
+ }
916
+
917
+ this._updateStyles();
918
+ if (this.infiniteScroll) {
919
+ this._clear();
920
+ }
921
+
922
+ this._load(this.nodeId);
923
+
924
+ // Don't update _lastLoadedId here - only update it in _handleContent after content is actually loaded
925
+ // This ensures _lastLoadedId only tracks what was actually loaded, not what we're trying to load
926
+ }
927
+
928
+ _load(pos, direction) {
929
+ const doc = this.getDocument ? this.getDocument() : null;
930
+
931
+ // In smoke/CT, pb-view may be mounted without a pb-document; bail safely
932
+ if (!doc || !doc.path) {
933
+ console.warn('<pb-view> No path');
934
+ return;
633
935
  }
634
936
 
635
- _doLoad(params) {
636
- this.emitTo('pb-start-update', params);
937
+ if (this._loading && this._lastRequestKey) {
938
+ const testParams = this.getParameters(pos);
939
+ const testReqKey = JSON.stringify({ url: this.url || '', doc: doc.path, params: testParams });
940
+ if (this._lastRequestKey === testReqKey) {
941
+ return;
942
+ }
943
+ // Cancel the pending request
944
+ const loader = this.shadowRoot.getElementById('loadContent');
945
+ if (loader) {
946
+ loader.abort();
947
+ }
948
+ // Clear the old request key so new requests aren't blocked
949
+ this._lastRequestKey = null;
950
+ this._loading = false;
951
+ }
952
+ this._loading = true;
637
953
 
638
- console.log("<pb-view> Loading view with params %o", params);
639
- if (!this.infiniteScroll) {
640
- this._clear();
641
- }
954
+ const params = this.getParameters(pos);
955
+ if (direction) {
956
+ params._dir = direction;
957
+ }
642
958
 
643
- if (this._scrollObserver) {
644
- if (this._bottomObserver) {
645
- this._scrollObserver.unobserve(this._bottomObserver);
646
- }
647
- if (this._topObserver) {
648
- this._scrollObserver.unobserve(this._topObserver);
649
- }
650
- }
959
+ this._doLoad(params);
960
+ }
961
+
962
+ _doLoad(params) {
963
+ // De-duplicate identical requests to avoid hammering the backend
964
+ const docPath = this.getDocument && this.getDocument() ? this.getDocument().path : '';
965
+ const reqKey = JSON.stringify({ url: this.url || '', doc: docPath, params });
966
+ if (this._lastRequestKey === reqKey) {
967
+ // nothing changed; unlock and skip
968
+ this._loading = false;
969
+ return;
970
+ }
971
+ this._lastRequestKey = reqKey;
651
972
 
652
- const loadContent = this.shadowRoot.getElementById('loadContent');
653
-
654
- if (this.static !== null) {
655
- this._staticUrl(params).then((url) => {
656
- loadContent.url = url;
657
- loadContent.generateRequest();
658
- });
659
- return;
660
- }
973
+ this.emitTo('pb-start-update', params);
661
974
 
662
- if (!this.url) {
663
- if (this.minApiVersion('1.0.0')) {
664
- this.url = "api/parts";
665
- } else {
666
- this.url = "modules/lib/components.xql";
667
- }
668
- }
975
+ if (!this.infiniteScroll) {
976
+ this._clear();
977
+ }
669
978
 
670
- let url = `${this.getEndpoint()}/${this.url}`;
979
+ if (this._scrollObserver) {
980
+ if (this._bottomObserver) {
981
+ this._scrollObserver.unobserve(this._bottomObserver);
982
+ }
983
+ if (this._topObserver) {
984
+ this._scrollObserver.unobserve(this._topObserver);
985
+ }
986
+ }
671
987
 
672
- if (this.minApiVersion('1.0.0')) {
673
- url += `/${encodeURIComponent(this.getDocument().path)}/json`;
674
- }
988
+ const loadContent = this.shadowRoot.getElementById('loadContent');
675
989
 
990
+ if (this.static !== null) {
991
+ this._staticUrl(params).then(url => {
676
992
  loadContent.url = url;
677
- loadContent.params = params;
678
- loadContent.generateRequest();
679
- }
680
-
681
- /**
682
- * Use a static URL to load pre-generated content.
683
- */
684
- async _staticUrl(params) {
685
- function createKey(paramNames) {
686
- const urlComponents = [];
687
- paramNames.sort().forEach(key => {
688
- if (params.hasOwnProperty(key)) {
689
- urlComponents.push(`${key}=${params[key]}`);
690
- }
691
- });
692
- return urlComponents.join('&');
693
- }
694
-
695
- const index = await fetch(`index.json`)
696
- .then((response) => response.json());
697
- const paramNames = ['odd', 'view', 'xpath', 'map'];
698
- this.querySelectorAll('pb-param').forEach((param) => paramNames.push(`user.${param.getAttribute('name')}`));
699
- let url = params.id ? createKey([...paramNames, 'id']) : createKey([...paramNames, 'root']);
700
- let file = index[url];
701
- if (!file) {
702
- url = createKey(paramNames);
703
- file = index[url];
704
- }
705
-
706
- console.log('<pb-view> Static lookup %s: %s', url, file);
707
- return `${file}`;
993
+ loadContent.generateRequest().catch(() => {
994
+ // Error handled by @error event listener
995
+ });
996
+ });
997
+ return;
708
998
  }
709
999
 
710
- _clear() {
711
- if (this.infiniteScroll) {
712
- this._content = document.createElement('div');
713
- this._content.className = 'infinite-content';
714
- } else {
715
- this._content = null;
716
- }
717
- this._column1 = null;
718
- this._column2 = null;
719
- this._footnotes = null;
720
- this._chunks = [];
1000
+ if (!this.url) {
1001
+ if (this.minApiVersion('1.0.0')) {
1002
+ this.url = 'api/parts';
1003
+ } else {
1004
+ this.url = 'modules/lib/components.xql';
1005
+ }
721
1006
  }
722
1007
 
723
- _handleError() {
724
- this._clear();
725
- const loader = this.shadowRoot.getElementById('loadContent');
726
- let message;
727
- const { response } = loader.lastError;
728
-
729
- if (response) {
730
- message = response.description;
731
- } else {
732
- message = '<pb-i18n key="dialogs.serverError"></pb-i18n>';
733
- }
734
-
735
- let content;
736
- if (this.notFound != null) {
737
- content = `<p>${this.notFound}</p>`;
738
- } else {
739
- content = `<p><pb-i18n key="dialogs.serverError"></pb-i18n>: ${message} </p>`;
740
- }
741
-
742
- this._replaceContent({ content });
743
- this.emitTo('pb-end-update');
1008
+ let url = `${this.getEndpoint()}/${this.url}`;
1009
+
1010
+ if (this.minApiVersion('1.0.0')) {
1011
+ // Encode the entire path as a single unit for the API
1012
+ const doc = this.getDocument();
1013
+ if (doc && doc.path) {
1014
+ const docPath = encodeURIComponent(doc.path);
1015
+ url += `/${docPath}/json`;
1016
+ } else {
1017
+ console.warn('<pb-view> No document path available for URL construction');
1018
+ return;
1019
+ }
1020
+ }
744
1021
 
1022
+ loadContent.url = url;
1023
+ loadContent.params = params;
1024
+ loadContent.generateRequest().catch((error) => {
1025
+ console.error('[pb-view] _doLoad: request failed', error);
1026
+ // Error handled by @error event listener
1027
+ });
1028
+ }
1029
+
1030
+ /**
1031
+ * Use a static URL to load pre-generated content.
1032
+ */
1033
+ async _staticUrl(params) {
1034
+ function createKey(paramNames) {
1035
+ const urlComponents = [];
1036
+ paramNames.sort().forEach(key => {
1037
+ if (params.hasOwnProperty(key)) {
1038
+ urlComponents.push(`${key}=${params[key]}`);
1039
+ }
1040
+ });
1041
+ return urlComponents.join('&');
745
1042
  }
746
1043
 
747
- _handleContent() {
748
- const loader = this.shadowRoot.getElementById('loadContent');
749
- const resp = loader.lastResponse;
1044
+ const baseDir = this.static ? this.static.replace(/\/$/, '') : '.';
1045
+ const baseUrl = new URL(`${baseDir}/`, window.location.href);
1046
+ const indexUrl = new URL('index.json', baseUrl).href;
1047
+ const index = await fetch(indexUrl).then(response => response.json());
1048
+ const paramNames = ['odd', 'view', 'xpath', 'map'];
1049
+ this.querySelectorAll('pb-param').forEach(param =>
1050
+ paramNames.push(`user.${param.getAttribute('name')}`),
1051
+ );
1052
+ let url = params.id ? createKey([...paramNames, 'id']) : createKey([...paramNames, 'root']);
1053
+ let file = index[url];
1054
+ if (!file) {
1055
+ url = createKey(paramNames);
1056
+ file = index[url];
1057
+ }
750
1058
 
751
- if (!resp) {
752
- console.error('<pb-view> No response received');
753
- return;
754
- }
755
- if (resp.error) {
756
- if (this.notFound != null) {
757
- this._content = this.notFound;
758
- }
759
- this.emitTo('pb-end-update', null);
760
- return;
761
- }
1059
+ if (!file) {
1060
+ console.warn('<pb-view> No static mapping found for %s', url);
1061
+ const fallback = Object.values(index)[0];
1062
+ if (!fallback) {
1063
+ return baseUrl.href;
1064
+ }
1065
+ file = fallback;
1066
+ }
1067
+ return new URL(file, baseUrl).href;
1068
+ }
1069
+
1070
+ _clear() {
1071
+ if (this.infiniteScroll) {
1072
+ this._content = document.createElement('div');
1073
+ this._content.className = 'infinite-content';
1074
+ } else {
1075
+ this._content = null;
1076
+ }
1077
+ this._column1 = null;
1078
+ this._column2 = null;
1079
+ this._footnotes = null;
1080
+ this._chunks = [];
1081
+ }
1082
+
1083
+ _handleError() {
1084
+ this._clear();
1085
+ this._loading = false;
1086
+ const loader = this.shadowRoot.getElementById('loadContent');
1087
+ console.error('<pb-view> Error details:', loader.lastError);
1088
+ let message;
1089
+ const { response } = loader.lastError;
1090
+
1091
+ if (response) {
1092
+ message = response.description;
1093
+ } else {
1094
+ message = '<pb-i18n key="dialogs.serverError"></pb-i18n>';
1095
+ }
762
1096
 
763
- this._replaceContent(resp, loader.params._dir);
1097
+ let content;
1098
+ if (this.notFound != null) {
1099
+ content = `<p>${this.notFound}</p>`;
1100
+ } else {
1101
+ content = `<p><pb-i18n key="dialogs.serverError"></pb-i18n>: ${message} </p>`;
1102
+ }
764
1103
 
765
- this.animate();
1104
+ this._replaceContent({ content });
1105
+ this.emitTo('pb-end-update');
1106
+ }
766
1107
 
767
- if (this._scrollTarget) {
768
- this.updateComplete.then(() => {
769
- const target = this.shadowRoot.getElementById(this._scrollTarget) ||
770
- this.shadowRoot.querySelector(`[node-id="${this._scrollTarget}"]`);
771
- if (target) {
772
- window.requestAnimationFrame(() =>
773
- setTimeout(() => {
774
- target.scrollIntoView({block: 'nearest'});
775
- }, 400)
776
- );
777
- }
778
- this._scrollTarget = null;
779
- });
780
- }
1108
+ _handleContent() {
1109
+ const loader = this.shadowRoot.getElementById('loadContent');
1110
+ const resp = loader.lastResponse;
781
1111
 
782
- this.next = resp.next;
783
- this.nextId = resp.nextId;
784
- this.previous = resp.previous;
785
- this.previousId = resp.previousId;
786
- this.nodeId = resp.root;
787
- this.switchView = resp.switchView;
788
-
789
- this.updateComplete.then(() => {
790
- const view = this.shadowRoot.getElementById('view');
791
- this._applyToggles(view);
792
- this._fixLinks(view);
793
- typesetMath(view);
794
-
795
- const eventOptions = {
796
- data: resp,
797
- root: view,
798
- params: loader.params,
799
- id: this.xmlId,
800
- position: this.nodeId
801
- };
802
- this.emitTo('pb-update', eventOptions);
803
- this._scroll();
804
- });
805
1112
 
806
- this.emitTo('pb-end-update', null);
1113
+ if (!resp) {
1114
+ this._loading = false;
1115
+ console.error('<pb-view> No response received');
1116
+ return;
1117
+ }
1118
+ if (resp.error) {
1119
+ console.error('<pb-view> Response has error:', resp.error);
1120
+ if (this.notFound != null) {
1121
+ this._content = this.notFound;
1122
+ }
1123
+ this.emitTo('pb-end-update', null);
1124
+ this._loading = false;
1125
+ return;
807
1126
  }
808
1127
 
809
- _replaceContent(resp, direction) {
810
- const fragment = document.createDocumentFragment();
811
- const elem = document.createElement('div');
812
- // elem.style.opacity = 0; //hide it - animation has to make sure to blend it in
813
- fragment.appendChild(elem);
814
- elem.innerHTML = resp.content;
1128
+ this._replaceContent(resp, loader.params._dir);
815
1129
 
816
- // if before-update-event is set, we do not replace the content immediately,
817
- // but emit an event
818
- if (this.beforeUpdate) {
819
- this.emitTo(this.beforeUpdate, {
820
- data: resp,
821
- root: elem,
822
- render: (content) => {
823
- this._doReplaceContent(content, resp, direction);
824
- }
825
- });
826
- } else {
827
- this._doReplaceContent(elem, resp, direction);
1130
+ this.animate();
1131
+
1132
+ if (this._scrollTarget) {
1133
+ this.updateComplete.then(() => {
1134
+ const target =
1135
+ this.shadowRoot.getElementById(this._scrollTarget) ||
1136
+ this.shadowRoot.querySelector(`[node-id="${this._scrollTarget}"]`);
1137
+ if (target) {
1138
+ window.requestAnimationFrame(() =>
1139
+ setTimeout(() => {
1140
+ target.scrollIntoView({ block: 'nearest' });
1141
+ }, 400),
1142
+ );
828
1143
  }
1144
+ this._scrollTarget = null;
1145
+ });
829
1146
  }
830
1147
 
831
- _doReplaceContent(elem, resp, direction) {
832
- if (this.columnSeparator) {
833
- this._replaceColumns(elem);
1148
+ this.next = resp.next;
1149
+ this.nextId = resp.nextId;
1150
+ this.previous = resp.previous;
1151
+ this.previousId = resp.previousId;
1152
+ this.nodeId = resp.root;
1153
+ this.switchView = resp.switchView;
1154
+
1155
+ this.updateComplete.then(() => {
1156
+ const view = this.shadowRoot.getElementById('view');
1157
+ this._applyToggles(view);
1158
+ this._fixLinks(view);
1159
+ typesetMath(view);
1160
+
1161
+ const eventOptions = {
1162
+ data: resp,
1163
+ root: view,
1164
+ params: loader.params,
1165
+ id: this.xmlId,
1166
+ position: this.nodeId,
1167
+ };
1168
+ this.emitTo('pb-update', eventOptions);
1169
+ this._scroll();
1170
+ });
1171
+
1172
+ this.emitTo('pb-end-update', null);
1173
+ // allow subsequent loads with new params
1174
+ this._loading = false;
1175
+ // Mark that this view has loaded at least once
1176
+ this._hasLoadedOnce = true;
1177
+ // Track the id that was actually loaded (for detecting navigation events)
1178
+ // This is critical - we only update _lastLoadedId when content is actually loaded,
1179
+ // not when registry just updates our state
1180
+ if (this.xmlId) {
1181
+ this._lastLoadedId = this.xmlId;
1182
+ }
1183
+ }
1184
+
1185
+ _replaceContent(resp, direction) {
1186
+ const fragment = document.createDocumentFragment();
1187
+ const elem = document.createElement('div');
1188
+ // elem.style.opacity = 0; //hide it - animation has to make sure to blend it in
1189
+ fragment.appendChild(elem);
1190
+ elem.innerHTML = resp.content;
1191
+
1192
+ // if before-update-event is set, we do not replace the content immediately,
1193
+ // but emit an event
1194
+ if (this.beforeUpdate) {
1195
+ this.emitTo(this.beforeUpdate, {
1196
+ data: resp,
1197
+ root: elem,
1198
+ render: content => {
1199
+ this._doReplaceContent(content, resp, direction);
1200
+ },
1201
+ });
1202
+ } else {
1203
+ this._doReplaceContent(elem, resp, direction);
1204
+ }
1205
+ }
1206
+
1207
+ _doReplaceContent(elem, resp, direction) {
1208
+
1209
+ if (this.columnSeparator) {
1210
+ this._replaceColumns(elem);
1211
+ this._loading = false;
1212
+ } else if (this.infiniteScroll) {
1213
+ elem.className = 'scroll-fragment';
1214
+ elem.setAttribute('data-root', resp.root);
1215
+ if (resp.next) {
1216
+ elem.setAttribute('data-next', resp.next);
1217
+ }
1218
+ if (resp.previous) {
1219
+ elem.setAttribute('data-previous', resp.previous);
1220
+ }
1221
+ let refNode;
1222
+ switch (direction) {
1223
+ case 'backward':
1224
+ refNode = this._content.firstElementChild;
1225
+ this._chunks.unshift(elem);
1226
+ this.updateComplete.then(() => {
1227
+ refNode.scrollIntoView(true);
834
1228
  this._loading = false;
835
- } else if (this.infiniteScroll) {
836
- elem.className = 'scroll-fragment';
837
- elem.setAttribute('data-root', resp.root);
838
- if (resp.next) {
839
- elem.setAttribute('data-next', resp.next);
840
- }
841
- if (resp.previous) {
842
- elem.setAttribute('data-previous', resp.previous);
843
- }
844
- let refNode;
845
- switch (direction) {
846
- case 'backward':
847
- refNode = this._content.firstElementChild;
848
- this._chunks.unshift(elem);
849
- this.updateComplete.then(() => {
850
- refNode.scrollIntoView(true);
851
- this._loading = false;
852
- this._checkVisibility();
853
- this._scrollObserver.observe(this._bottomObserver);
854
- this._scrollObserver.observe(this._topObserver);
855
- });
856
- this._content.insertBefore(elem, refNode);
857
- break;
858
- default:
859
- this.updateComplete.then(() => {
860
- this._loading = false;
861
- this._checkVisibility();
862
- this._scrollObserver.observe(this._bottomObserver);
863
- this._scrollObserver.observe(this._topObserver);
864
- });
865
- this._chunks.push(elem);
866
- this._content.appendChild(elem);
867
- break;
868
- }
869
- } else {
870
- this._content = elem;
1229
+ this._checkVisibility();
1230
+ this._scrollObserver.observe(this._bottomObserver);
1231
+ this._scrollObserver.observe(this._topObserver);
1232
+ });
1233
+ this._content.insertBefore(elem, refNode);
1234
+ break;
1235
+ default:
1236
+ this.updateComplete.then(() => {
871
1237
  this._loading = false;
872
- }
873
-
874
- if (this.appendFootnotes) {
875
- const footnotes = document.createElement('div');
876
- if (resp.footnotes) {
877
- footnotes.innerHTML = resp.footnotes;
878
- }
879
- this._footnotes = footnotes;
880
- }
881
-
882
- this._initFootnotes(this._footnotes);
1238
+ this._checkVisibility();
1239
+ this._scrollObserver.observe(this._bottomObserver);
1240
+ this._scrollObserver.observe(this._topObserver);
1241
+ });
1242
+ this._chunks.push(elem);
1243
+ this._content.appendChild(elem);
1244
+ break;
1245
+ }
1246
+ } else {
1247
+ this._content = elem;
1248
+ this._loading = false;
1249
+ }
883
1250
 
884
- return elem;
1251
+ if (this.appendFootnotes) {
1252
+ const footnotes = document.createElement('div');
1253
+ if (resp.footnotes) {
1254
+ footnotes.innerHTML = resp.footnotes;
1255
+ }
1256
+ this._footnotes = footnotes;
885
1257
  }
886
1258
 
887
- _checkVisibility() {
888
- const bottomActive = this._chunks[this._chunks.length - 1].hasAttribute('data-next');
889
- this._bottomObserver.style.display = bottomActive ? '' : 'none';
1259
+ this._initFootnotes(this._footnotes);
890
1260
 
891
- const topActive = this._chunks[0].hasAttribute('data-previous');
892
- this._topObserver.style.display = topActive ? '' : 'none';
893
- }
1261
+ return elem;
1262
+ }
894
1263
 
895
- _replaceColumns(elem) {
896
- let cb;
897
- if (this.columnSeparator) {
898
- const cbs = elem.querySelectorAll(this.columnSeparator);
899
- // use last separator only
900
- if (cbs.length > 1) {
901
- cb = cbs[cbs.length - 1];
902
- }
903
- }
1264
+ _checkVisibility() {
1265
+ const bottomActive = this._chunks[this._chunks.length - 1].hasAttribute('data-next');
1266
+ this._bottomObserver.style.display = bottomActive ? '' : 'none';
904
1267
 
905
- if (!cb) {
906
- this._content = elem;
907
- } else {
908
- const fragmentBefore = this._getFragmentBefore(elem, cb);
909
- const fragmentAfter = this._getFragmentAfter(elem, cb);
910
- if (this.direction === 'ltr') {
911
- this._column1 = fragmentBefore;
912
- this._column2 = fragmentAfter;
913
- } else {
914
- this._column2 = fragmentBefore;
915
- this._column1 = fragmentAfter;
916
- }
917
- }
1268
+ const topActive = this._chunks[0].hasAttribute('data-previous');
1269
+ this._topObserver.style.display = topActive ? '' : 'none';
1270
+ }
1271
+
1272
+ _replaceColumns(elem) {
1273
+ let cb;
1274
+ if (this.columnSeparator) {
1275
+ const cbs = elem.querySelectorAll(this.columnSeparator);
1276
+ // use last separator only
1277
+ if (cbs.length > 1) {
1278
+ cb = cbs[cbs.length - 1];
1279
+ }
918
1280
  }
919
1281
 
920
- _scroll() {
921
- if (this.noScroll) {
922
- return;
923
- }
924
- if (registry.hash) {
925
- const target = this.shadowRoot.getElementById(registry.hash.substring(1));
926
- console.log('hash target: %o', target);
927
- if (target) {
928
- window.requestAnimationFrame(() =>
929
- setTimeout(() => {
930
- target.scrollIntoView({ block: "center", inline: "nearest" });
931
- }, 400)
932
- );
933
- }
934
- }
1282
+ if (!cb) {
1283
+ this._content = elem;
1284
+ } else {
1285
+ const fragmentBefore = this._getFragmentBefore(elem, cb);
1286
+ const fragmentAfter = this._getFragmentAfter(elem, cb);
1287
+ if (this.direction === 'ltr') {
1288
+ this._column1 = fragmentBefore;
1289
+ this._column2 = fragmentAfter;
1290
+ } else {
1291
+ this._column2 = fragmentBefore;
1292
+ this._column1 = fragmentAfter;
1293
+ }
935
1294
  }
1295
+ }
936
1296
 
937
- _scrollToElement(ev, link) {
938
- const target = this.shadowRoot.getElementById(link.hash.substring(1));
939
- if (target) {
940
- ev.preventDefault();
941
- console.log('<pb-view> Scrolling to element %s', target.id);
942
- target.scrollIntoView({ block: "center", inline: "nearest" });
943
- }
1297
+ _scroll() {
1298
+ if (this.noScroll) {
1299
+ return;
944
1300
  }
1301
+ if (registry.hash) {
1302
+ const target = this.shadowRoot.getElementById(registry.hash.substring(1));
1303
+ if (target) {
1304
+ window.requestAnimationFrame(() =>
1305
+ setTimeout(() => {
1306
+ target.scrollIntoView({ block: 'center', inline: 'nearest' });
1307
+ }, 400),
1308
+ );
1309
+ }
1310
+ }
1311
+ }
945
1312
 
946
- _updateStyles() {
947
- let link = document.createElement('link');
948
- link.setAttribute('rel', 'stylesheet');
949
- link.setAttribute('type', 'text/css');
950
- if (this.static !== null) {
951
- link.setAttribute('href', `${this.static}/css/${this.getOdd()}.css`);
952
- } else {
953
- link.setAttribute('href', `${this.getEndpoint()}/transform/${this.getOdd()}.css`);
954
- }
955
- this._style = link;
956
- }
957
-
958
- _fixLinks(content) {
959
- if (this.fixLinks) {
960
- const doc = this.getDocument();
961
- const base = this.toAbsoluteURL(doc.path);
962
- content.querySelectorAll('img').forEach((image) => {
963
- const oldSrc = image.getAttribute('src');
964
- const src = new URL(oldSrc, base);
965
- image.src = src;
966
- });
967
- content.querySelectorAll('a').forEach((link) => {
968
- const oldHref = link.getAttribute('href');
969
- if (oldHref === link.hash) {
970
- link.addEventListener('click', (ev) => this._scrollToElement(ev, link));
971
- } else {
972
- const href = new URL(oldHref, base);
973
- link.href = href;
974
- }
975
- });
1313
+ _scrollToElement(ev, link) {
1314
+ const target = this.shadowRoot.getElementById(link.hash.substring(1));
1315
+ if (target) {
1316
+ ev.preventDefault();
1317
+ target.scrollIntoView({ block: 'center', inline: 'nearest' });
1318
+ }
1319
+ }
1320
+
1321
+ _updateStyles() {
1322
+ const link = document.createElement('link');
1323
+ link.setAttribute('rel', 'stylesheet');
1324
+ link.setAttribute('type', 'text/css');
1325
+ if (this.static !== null) {
1326
+ link.setAttribute('href', `${this.static}/css/${this.getOdd()}.css`);
1327
+ } else {
1328
+ link.setAttribute('href', `${this.getEndpoint()}/transform/${this.getOdd()}.css`);
1329
+ }
1330
+ this._style = link;
1331
+ }
1332
+
1333
+ _fixLinks(content) {
1334
+ if (this.fixLinks) {
1335
+ const doc = this.getDocument ? this.getDocument() : null;
1336
+ const base = this.toAbsoluteURL(doc && doc.path ? doc.path : '');
1337
+ content.querySelectorAll('img').forEach(image => {
1338
+ const oldSrc = image.getAttribute('src');
1339
+ const src = new URL(oldSrc, base);
1340
+ image.src = src.href;
1341
+ });
1342
+ content.querySelectorAll('a').forEach(link => {
1343
+ const oldHref = link.getAttribute('href');
1344
+ if (oldHref === link.hash) {
1345
+ link.addEventListener('click', ev => this._scrollToElement(ev, link));
976
1346
  } else {
977
- content.querySelectorAll('a').forEach((link) => {
978
- const oldHref = link.getAttribute('href');
979
- if (oldHref === link.hash) {
980
- link.addEventListener('click', (ev) => this._scrollToElement(ev, link));
981
- }
982
- });
1347
+ const href = new URL(oldHref, base);
1348
+ link.href = href.href;
983
1349
  }
984
- }
985
-
986
- _initFootnotes(content) {
987
- if (content) {
988
- content.querySelectorAll('.note, .fn-back').forEach(elem => {
989
- elem.addEventListener('click', (ev) => {
990
- ev.preventDefault();
991
- const fn = this.shadowRoot.getElementById('content').querySelector(elem.hash);
992
- if (fn) {
993
- fn.scrollIntoView();
994
- }
995
- });
996
- });
1350
+ });
1351
+ } else {
1352
+ content.querySelectorAll('a').forEach(link => {
1353
+ const oldHref = link.getAttribute('href');
1354
+ if (oldHref === link.hash) {
1355
+ link.addEventListener('click', ev => this._scrollToElement(ev, link));
997
1356
  }
1357
+ });
998
1358
  }
999
-
1000
- _getParameters() {
1001
- const params = [];
1002
- this.querySelectorAll('pb-param').forEach(function (param) {
1003
- params['user.' + param.getAttribute('name')] = param.getAttribute('value');
1359
+ }
1360
+
1361
+ _initFootnotes(content) {
1362
+ if (content) {
1363
+ content.querySelectorAll('.note, .fn-back').forEach(elem => {
1364
+ elem.addEventListener('click', ev => {
1365
+ ev.preventDefault();
1366
+ const fn = this.shadowRoot.getElementById('content').querySelector(elem.hash);
1367
+ if (fn) {
1368
+ fn.scrollIntoView();
1369
+ }
1004
1370
  });
1005
- // add parameters for features set with pb-toggle-feature
1006
- for (const [key, value] of Object.entries(this._features)) {
1007
- params['user.' + key] = value;
1008
- }
1009
- // add parameters for user-defined parameters supplied via pb-link
1010
- if (this._additionalParams) {
1011
- for (const [key, value] of Object.entries(this._additionalParams)) {
1012
- params[key] = value;
1013
- }
1014
- }
1015
- return params;
1016
- }
1017
-
1018
- /**
1019
- * Return the parameter object which would be passed to the server by this component
1020
- */
1021
- getParameters(pos) {
1022
- pos = pos || this.nodeId;
1023
- const doc = this.getDocument();
1024
- const params = this._getParameters();
1025
- if (!this.minApiVersion('1.0.0')) {
1026
- params.doc = doc.path;
1027
- }
1028
- params.odd = this.getOdd() + '.odd';
1029
- params.view = this.getView();
1030
- if (pos) {
1031
- params['root'] = pos;
1032
- }
1033
- if (this.xpath) {
1034
- params.xpath = this.xpath;
1035
- }
1036
- if (this.xmlId) {
1037
- params.id = this.xmlId;
1038
- }
1039
- if (!this.suppressHighlight && this.highlight) {
1040
- params.highlight = "yes";
1041
- }
1042
- if (this.map) {
1043
- params.map = this.map;
1044
- }
1045
-
1046
- return params;
1371
+ });
1047
1372
  }
1048
-
1049
- _applyToggles(elem) {
1050
- for (const [selector, setting] of Object.entries(this._selector)) {
1051
- elem.querySelectorAll(selector).forEach(node => {
1052
- const command = setting.command || 'toggle';
1053
- if (node.command) {
1054
- node.command(command, setting.state);
1055
- }
1056
- if (setting.state) {
1057
- node.classList.add(command);
1058
- } else {
1059
- node.classList.remove(command);
1060
- }
1061
- });
1062
- }
1373
+ }
1374
+
1375
+ _getParameters() {
1376
+ const params = {}; // Use object, not array
1377
+ this.querySelectorAll('pb-param').forEach(param => {
1378
+ const name = param.getAttribute('name');
1379
+ const value = param.getAttribute('value');
1380
+ // For metadata panel, filter out canvas IDs
1381
+ if (name === 'id') {
1382
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(value));
1383
+ if (looksLikeCanvasId) {
1384
+ // Check if this is a metadata panel
1385
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1386
+ if (modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel') {
1387
+ return; // Skip canvas IDs for metadata panel
1388
+ }
1389
+ }
1390
+ }
1391
+ params[`user.${name}`] = value;
1392
+ });
1393
+ // add parameters for features set with pb-toggle-feature
1394
+ for (const [key, value] of Object.entries(this._features)) {
1395
+ // For metadata panel, filter out canvas IDs
1396
+ if (key === 'id') {
1397
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(value));
1398
+ if (looksLikeCanvasId) {
1399
+ // Check if this is a metadata panel
1400
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1401
+ if (modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel') {
1402
+ continue; // Skip canvas IDs for metadata panel
1403
+ }
1404
+ }
1405
+ }
1406
+ params[`user.${key}`] = value;
1063
1407
  }
1064
-
1065
- /**
1066
- * Load a part of the document identified by the given eXist nodeId
1067
- *
1068
- * @param {String} nodeId The eXist nodeId of the root element to load
1069
- */
1070
- goto(nodeId) {
1071
- this._load(nodeId);
1072
- }
1073
-
1074
- /**
1075
- * Load a part of the document identified by the given xml:id
1076
- *
1077
- * @param {String} xmlId The xml:id to be loaded
1078
- */
1079
- gotoId(xmlId) {
1080
- this.xmlId = xmlId;
1081
- this._load();
1082
- }
1083
-
1084
- /**
1085
- * Navigate the document either forward or backward and refresh the view.
1086
- * The navigation method is determined by property `view`.
1087
- *
1088
- * @param {string} direction either `backward` or `forward`
1089
- */
1090
- navigate(direction) {
1091
- // in single view mode there should be no navigation
1092
- if (this.getView() === 'single') {
1093
- return;
1094
- }
1095
-
1096
- this.lastDirection = direction;
1097
-
1098
- if (direction === 'backward') {
1099
- if (this.previous) {
1100
- if (!this.disableHistory && !this.map) {
1101
- registry.commit(this, {
1102
- id: this.previousId || null,
1103
- root: this.previousId ? null : this.previous
1104
- });
1105
- }
1106
- this.xmlId = this.previousId;
1107
- this._load(this.xmlId ? null : this.previous, direction);
1408
+ // add parameters for user-defined parameters supplied via pb-link
1409
+ if (this._additionalParams) {
1410
+ for (const [key, value] of Object.entries(this._additionalParams)) {
1411
+ // For metadata panel, filter out canvas IDs
1412
+ if (key === 'id' || key === 'user.id') {
1413
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(value));
1414
+ if (looksLikeCanvasId) {
1415
+ // Check if this is a metadata panel
1416
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1417
+ if (modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel') {
1418
+ continue; // Skip canvas IDs for metadata panel
1108
1419
  }
1109
- } else if (this.next) {
1110
- if (!this.disableHistory && !this.map) {
1111
- registry.commit(this, {
1112
- id: this.nextId || null,
1113
- root: this.nextId ? null : this.next
1114
- });
1115
- }
1116
- this.xmlId = this.nextId;
1117
- this._load(this.xmlId ? null : this.next, direction);
1420
+ }
1118
1421
  }
1422
+ params[key] = value;
1423
+ }
1119
1424
  }
1120
-
1121
- /**
1122
- * Check the number of fragments which were already loaded in infinite
1123
- * scroll mode. If they exceed `infiniteScrollMax`, remove either the
1124
- * first or last fragment from the DOM, depending on the scroll direction.
1125
- *
1126
- * @param {string} direction either 'forward' or 'backward'
1127
- */
1128
- _checkChunks(direction) {
1129
- if (!this.infiniteScroll || this.infiniteScrollMax === 0) {
1130
- return;
1131
- }
1132
-
1133
- if (this._chunks.length === this.infiniteScrollMax) {
1134
- switch (direction) {
1135
- case 'forward':
1136
- this._content.removeChild(this._chunks.shift());
1137
- break;
1138
- default:
1139
- this._content.removeChild(this._chunks.pop());
1140
- break;
1141
- }
1142
- }
1143
- this.emitTo('pb-navigate', {
1144
- direction,
1145
- source: this
1146
- });
1425
+ return params;
1426
+ }
1427
+
1428
+ /**
1429
+ * Return the parameter object which would be passed to the server by this component
1430
+ */
1431
+ getParameters(pos) {
1432
+ pos = pos || this.nodeId;
1433
+ const doc = this.getDocument ? this.getDocument() : null;
1434
+ const params = this._getParameters();
1435
+ if (!this.minApiVersion('1.0.0') && doc && doc.path) {
1436
+ params.doc = doc.path;
1147
1437
  }
1148
-
1149
- /**
1150
- * Zoom the displayed content by increasing or decreasing font size.
1151
- *
1152
- * @param {string} direction either `in` or `out`
1153
- */
1154
- zoom(direction) {
1155
- const view = this.shadowRoot.getElementById('view');
1156
- const fontSize = window.getComputedStyle(view).getPropertyValue('font-size');
1157
- const size = parseInt(fontSize.replace(/^(\d+)px/, "$1"));
1158
-
1159
- if (direction === 'in') {
1160
- view.style.fontSize = (size + 1) + 'px';
1161
- } else {
1162
- view.style.fontSize = (size - 1) + 'px';
1163
- }
1438
+ params.odd = `${this.getOdd()}.odd`;
1439
+ // For metadata panel, use 'single' view to ensure teiHeader is returned
1440
+ // and don't set root parameter - it should return teiHeader, not a specific page
1441
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1442
+ const isMetadataPanel = modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel';
1443
+ if (isMetadataPanel) {
1444
+ params.view = 'single';
1445
+ } else {
1446
+ params.view = this.getView();
1164
1447
  }
1165
-
1166
- toggleFeature(ev) {
1167
- const properties = registry.getState(this);
1168
- if (properties) {
1169
- this._setState(properties);
1170
-
1171
- }
1172
-
1173
- if (ev.detail.refresh) {
1174
- this._updateStyles();
1175
- this._load();
1176
- } else {
1177
- const view = this.shadowRoot.getElementById('view');
1178
- this._applyToggles(view);
1179
- }
1180
- registry.commit(this, properties);
1448
+ params.fill = this.fill;
1449
+ if (pos && !isMetadataPanel) {
1450
+ params.root = pos;
1451
+ }
1452
+ if (this.xpath) {
1453
+ params.xpath = this.xpath;
1454
+ }
1455
+ // Check if we have an id in _additionalParams first (from registry/event, including canvas IDs)
1456
+ // This takes precedence over xmlId because it's the most recent state
1457
+ if (this._additionalParams && this._additionalParams.id) {
1458
+ // For metadata panel, don't add canvas IDs
1459
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1460
+ const isMetadataPanel = modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel';
1461
+ if (isMetadataPanel) {
1462
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(this._additionalParams.id));
1463
+ if (!looksLikeCanvasId) {
1464
+ params.id = this._additionalParams.id;
1465
+ }
1466
+ } else {
1467
+ params.id = this._additionalParams.id;
1468
+ }
1469
+ } else if (this.xmlId) {
1470
+ // Fall back to xmlId if no id in _additionalParams
1471
+ // For metadata panel, don't add canvas IDs
1472
+ const modeParamEl = this.querySelector('pb-param[name="mode"]');
1473
+ const isMetadataPanel = modeParamEl && modeParamEl.getAttribute('value') === 'metadata-panel';
1474
+ if (isMetadataPanel) {
1475
+ const looksLikeCanvasId = /\.jpg$|_\d{2,3}\.jpg/.test(String(this.xmlId));
1476
+ if (!looksLikeCanvasId) {
1477
+ params.id = this.xmlId;
1478
+ }
1479
+ } else {
1480
+ params.id = this.xmlId;
1481
+ }
1482
+ }
1483
+ if (!this.suppressHighlight && this.highlight) {
1484
+ params.highlight = 'yes';
1485
+ }
1486
+ if (this.map) {
1487
+ params.map = this.map;
1181
1488
  }
1182
1489
 
1183
- _setState(properties) {
1184
- for (const [key, value] of Object.entries(properties)) {
1185
- // check if URL template needs the parameter and if
1186
- // yes, add it to the additional parameter list
1187
- if (registry.pathParams.has(key)) {
1188
- this._additionalParams[key] = value;
1189
- } else {
1190
- switch (key) {
1191
- case 'odd':
1192
- case 'view':
1193
- case 'columnSeparator':
1194
- case 'xpath':
1195
- case 'nodeId':
1196
- case 'path':
1197
- case 'root':
1198
- break;
1199
- default:
1200
- this._features[key] = value;
1201
- break;
1202
- }
1203
- }
1204
- }
1205
- if (properties.odd && !this.getAttribute('odd')) {
1206
- this.odd = properties.odd;
1207
- }
1208
- if (properties.view && !this.getAttribute('view')) {
1209
- this.view = properties.view;
1210
- if (this.view === 'single') {
1211
- // when switching to single view, clear current node id
1212
- this.nodeId = null;
1213
- } else {
1214
- // otherwise use value for alternate view returned from server
1215
- this.nodeId = this.switchView;
1216
- }
1217
- }
1218
- if (properties.xpath && !this.getAttribute('xpath')) {
1219
- this.xpath = properties.xpath;
1220
- }
1221
- if (properties.hasOwnProperty('columnSeparator')) {
1222
- this.columnSeparator = properties.columnSeparator;
1223
- }
1224
- this.xmlId = (!this.getAttribute('xml-id') && properties.id) || this.xmlId;
1225
- this.nodeId = (!this.getAttribute('xml-id') && properties.root) || null;
1490
+ return params;
1491
+ }
1226
1492
 
1227
- if (properties.path) {
1228
- this.getDocument().path = properties.path;
1493
+ _applyToggles(elem) {
1494
+ for (const [selector, setting] of Object.entries(this._selector)) {
1495
+ elem.querySelectorAll(selector).forEach(node => {
1496
+ const command = setting.command || 'toggle';
1497
+ if (node.command) {
1498
+ node.command(command, setting.state);
1229
1499
  }
1230
-
1231
- if (properties.selectors) {
1232
- properties.selectors.forEach(sc => {
1233
- this._selector[sc.selector] = {
1234
- state: sc.state,
1235
- command: sc.command || 'toggle'
1236
- };
1237
- });
1500
+ if (setting.state) {
1501
+ node.classList.add(command);
1502
+ } else {
1503
+ node.classList.remove(command);
1238
1504
  }
1505
+ });
1239
1506
  }
1240
-
1241
- _getFragmentBefore(node, ms) {
1242
- const range = document.createRange();
1243
- range.setStartBefore(node);
1244
- range.setEndBefore(ms);
1245
-
1246
- return range.cloneContents();
1507
+ }
1508
+
1509
+ /**
1510
+ * Load a part of the document identified by the given eXist nodeId
1511
+ *
1512
+ * @param {String} nodeId The eXist nodeId of the root element to load
1513
+ */
1514
+ goto(nodeId) {
1515
+ this._load(nodeId);
1516
+ }
1517
+
1518
+ /**
1519
+ * Load a part of the document identified by the given xml:id
1520
+ *
1521
+ * @param {String} xmlId The xml:id to be loaded
1522
+ */
1523
+ gotoId(xmlId) {
1524
+ this.xmlId = xmlId;
1525
+ this._load();
1526
+ }
1527
+
1528
+ /**
1529
+ * Navigate the document either forward or backward and refresh the view.
1530
+ * The navigation method is determined by property `view`.
1531
+ *
1532
+ * @param {string} direction either `backward` or `forward`
1533
+ */
1534
+ navigate(direction) {
1535
+ // in single view mode there should be no navigation
1536
+ if (this.getView() === 'single') {
1537
+ return;
1247
1538
  }
1248
1539
 
1249
- _getFragmentAfter(node, ms) {
1250
- const range = document.createRange();
1251
- range.setStartBefore(ms);
1252
- range.setEndAfter(node);
1253
-
1254
- return range.cloneContents();
1540
+ this.lastDirection = direction;
1541
+
1542
+ if (direction === 'backward') {
1543
+ if (this.previous) {
1544
+ const isReadOnly = this.readOnlyRegistry || this.hasAttribute('read-only-registry');
1545
+ if (!this.disableHistory && !this.map && !isReadOnly) {
1546
+ registry.commit(this, {
1547
+ id: this.previousId || null,
1548
+ root: this.previousId ? null : this.previous,
1549
+ });
1550
+ }
1551
+ this.xmlId = this.previousId;
1552
+ this._load(this.xmlId ? null : this.previous, direction);
1553
+ }
1554
+ } else if (this.next) {
1555
+ const isReadOnly = this.readOnlyRegistry || this.hasAttribute('read-only-registry');
1556
+ if (!this.disableHistory && !this.map && !isReadOnly) {
1557
+ registry.commit(this, {
1558
+ id: this.nextId || null,
1559
+ root: this.nextId ? null : this.next,
1560
+ });
1561
+ }
1562
+ this.xmlId = this.nextId;
1563
+ this._load(this.xmlId ? null : this.next, direction);
1255
1564
  }
1256
-
1257
- _updateSource(newVal, oldVal) {
1258
- if (typeof oldVal !== 'undefined' && newVal !== oldVal) {
1259
- this.xpath = null;
1260
- this.odd = null;
1261
- this.xmlId = null;
1262
- this.nodeId = null;
1263
- }
1565
+ }
1566
+
1567
+ /**
1568
+ * Check the number of fragments which were already loaded in infinite
1569
+ * scroll mode. If they exceed `infiniteScrollMax`, remove either the
1570
+ * first or last fragment from the DOM, depending on the scroll direction.
1571
+ *
1572
+ * @param {string} direction either 'forward' or 'backward'
1573
+ */
1574
+ _checkChunks(direction) {
1575
+ if (!this.infiniteScroll || this.infiniteScrollMax === 0) {
1576
+ return;
1264
1577
  }
1265
1578
 
1266
- static get styles() {
1267
- return css`
1268
- :host {
1269
- display: block;
1270
- background: transparent;
1271
- }
1272
-
1273
- :host(.noscroll) {
1274
- scrollbar-width: none; /* Firefox 64 */
1275
- -ms-overflow-style: none;
1276
- }
1277
-
1278
- :host(.noscroll)::-webkit-scrollbar {
1279
- width: 0 !important;
1280
- display: none;
1281
- }
1282
-
1283
- [id] {
1284
- scroll-margin-top: var(--pb-view-scroll-margin-top);
1285
- }
1286
-
1287
- #view {
1288
- position: relative;
1289
- }
1290
-
1291
- .columns {
1292
- display: grid;
1293
- grid-template-columns: calc(50% - var(--pb-view-column-gap, 10px) / 2) calc(50% - var(--pb-view-column-gap, 10px) / 2);
1294
- grid-column-gap: var(--pb-view-column-gap, 10px);
1295
- }
1296
-
1297
- .margin-note {
1298
- display: none;
1299
- }
1300
-
1301
- @media (min-width: 769px) {
1302
- .content.margin-right {
1303
- margin-right: 200px;
1304
- }
1305
-
1306
- .margin-note {
1307
- background: rgba(153, 153, 153, 0.2);
1308
- display: block;
1309
- font-size: small;
1310
- margin-right: -200px;
1311
- margin-bottom: 5px;
1312
- padding: 5px 0;
1313
- float: right;
1314
- clear: both;
1315
- width: 180px;
1316
- }
1317
-
1318
- .margin-note .n {
1319
- color: #777777;
1320
- }
1321
- }
1322
-
1323
- a[rel=footnote] {
1324
- font-size: var(--pb-footnote-font-size, var(--pb-content-font-size, 75%));
1325
- font-family: var(--pb-footnote-font-family, --pb-content-font-family);
1326
- vertical-align: super;
1327
- color: var(--pb-footnote-color, var(--pb-color-primary, #333333));
1328
- text-decoration: none;
1329
- padding: var(--pb-footnote-padding, 0 0 0 .25em);
1330
- }
1331
-
1332
- .list dt {
1333
- float: left;
1334
- }
1335
-
1336
- .footnote .fn-number {
1337
- float: left;
1338
- font-size: var(--pb-footnote-font-size, var(--pb-content-font-size, 75%));
1339
- }
1340
-
1341
- .observer {
1342
- display: block;
1343
- width: 100%;
1344
- height: var(--pb-view-loader-height, 16px);
1345
- font-family: var(--pb-view-loader-font, --pb-base-font);
1346
- color: var(--pb-view-loader-color, black);
1347
- background: var(--pb-view-loader-background, #909090);
1348
- background-image: var(--pb-view-loader-background-image, repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(255,255,255,.5) 35px, rgba(255,255,255,.5) 70px));
1349
- animation-name: loader;
1350
- animation-timing-function: linear;
1351
- animation-duration: 2s;
1352
- animation-fill-mode: forwards;
1353
- animation-iteration-count: infinite;
1354
- }
1579
+ if (this._chunks.length === this.infiniteScrollMax) {
1580
+ switch (direction) {
1581
+ case 'forward':
1582
+ this._content.removeChild(this._chunks.shift());
1583
+ break;
1584
+ default:
1585
+ this._content.removeChild(this._chunks.pop());
1586
+ break;
1587
+ }
1588
+ }
1589
+ this.emitTo('pb-navigate', {
1590
+ direction,
1591
+ source: this,
1592
+ });
1593
+ }
1594
+
1595
+ toggleFeature(ev) {
1596
+ const properties = registry.getState(this);
1597
+ if (properties) {
1598
+ this._setState(properties);
1599
+ }
1355
1600
 
1356
- @keyframes loader {
1357
- 0% {
1358
- background-position: 3rem 0;
1359
- }
1360
-
1361
- 100% {
1362
- background-position: 0 0;
1363
- }
1364
- }
1601
+ if (ev.detail.refresh) {
1602
+ this._updateStyles();
1603
+ this._load();
1604
+ } else {
1605
+ const view = this.shadowRoot.getElementById('view');
1606
+ this._applyToggles(view);
1607
+ }
1608
+ // Only commit to registry if not in read-only mode
1609
+ // In read-only mode, pb-view only reads from registry, never writes
1610
+ // Check both property (LitElement) and attribute (XML/XHTML compatibility)
1611
+ const isReadOnly = this.readOnlyRegistry || this.hasAttribute('read-only-registry');
1612
+ if (!isReadOnly) {
1613
+ registry.commit(this, properties);
1614
+ }
1615
+ }
1616
+
1617
+ _setState(properties) {
1618
+ for (const [key, value] of Object.entries(properties)) {
1619
+ // check if URL template needs the parameter and if
1620
+ // yes, add it to the additional parameter list
1621
+ if (registry.pathParams.has(key)) {
1622
+ this._additionalParams[key] = value;
1623
+ } else {
1624
+ switch (key) {
1625
+ case 'odd':
1626
+ case 'view':
1627
+ case 'columnSeparator':
1628
+ case 'xpath':
1629
+ case 'nodeId':
1630
+ case 'path':
1631
+ case 'root':
1632
+ break;
1633
+ default:
1634
+ this._features[key] = value;
1635
+ break;
1636
+ }
1637
+ }
1638
+ }
1639
+ if (properties.odd && !this.getAttribute('odd')) {
1640
+ this.odd = properties.odd;
1641
+ }
1642
+ if (properties.view && !this.getAttribute('view')) {
1643
+ this.view = properties.view;
1644
+ if (this.view === 'single') {
1645
+ // when switching to single view, clear current node id
1646
+ this.nodeId = null;
1647
+ } else {
1648
+ // otherwise use value for alternate view returned from server
1649
+ this.nodeId = this.switchView;
1650
+ }
1651
+ }
1652
+ if (properties.fill && !this.getAttribute('fill')) {
1653
+ this.fill = properties.fill;
1654
+ }
1655
+ if (properties.xpath && !this.getAttribute('xpath')) {
1656
+ this.xpath = properties.xpath;
1657
+ }
1658
+ if (properties.hasOwnProperty('columnSeparator')) {
1659
+ this.columnSeparator = properties.columnSeparator;
1660
+ }
1661
+ this.xmlId = (!this.getAttribute('xml-id') && properties.id) || this.xmlId;
1662
+ this.nodeId = (!this.getAttribute('xml-id') && properties.root) || null;
1365
1663
 
1366
- .scroll-fragment {
1367
- animation: fadeIn ease 500ms;
1368
- }
1664
+ if (properties.path) {
1665
+ const doc = this.getDocument ? this.getDocument() : null;
1666
+ if (doc) doc.path = properties.path;
1667
+ }
1369
1668
 
1370
- @keyframes fadeIn {
1371
- 0% {opacity:0;}
1372
- 100% {opacity:1;}
1373
- }
1374
- `;
1375
- }
1376
-
1377
- render() {
1378
- return [
1379
- html`
1380
- <div id="view" part="content">
1381
- ${this._style}
1382
- ${this.infiniteScroll ? html`<div id="top-observer" class="observer"></div>` : null}
1383
- <div class="columns">
1384
- <div id="column1">${this._column1}</div>
1385
- <div id="column2">${this._column2}</div>
1386
- </div>
1387
- <div id="content">${this._content}</div>
1388
- ${
1389
- this.infiniteScroll ?
1390
- html`<div id="bottom-observer" class="observer"></div>` :
1391
- null
1392
- }
1393
- <div id="footnotes" part="footnotes">${this._footnotes}</div>
1394
- </div>
1395
- <paper-dialog id="errorDialog">
1396
- <h2>${translate('dialogs.error')}</h2>
1397
- <paper-dialog-scrollable></paper-dialog-scrollable>
1398
- <div class="buttons">
1399
- <paper-button dialog-confirm="dialog-confirm" autofocus="autofocus">
1400
- ${translate('dialogs.close')}
1401
- </paper-button>
1402
- </div>
1403
- </paper-dialog>
1404
- <iron-ajax
1405
- id="loadContent"
1406
- verbose
1407
- handle-as="json"
1408
- method="get"
1409
- with-credentials
1410
- @response="${this._handleContent}"
1411
- @error="${this._handleError}"></iron-ajax>
1412
- `
1413
- ]
1669
+ if (properties.selectors) {
1670
+ properties.selectors.forEach(sc => {
1671
+ this._selector[sc.selector] = {
1672
+ state: sc.state,
1673
+ command: sc.command || 'toggle',
1674
+ };
1675
+ });
1676
+ }
1677
+ }
1678
+
1679
+ _getFragmentBefore(node, ms) {
1680
+ const range = document.createRange();
1681
+ range.setStartBefore(node);
1682
+ range.setEndBefore(ms);
1683
+
1684
+ return range.cloneContents();
1685
+ }
1686
+
1687
+ _getFragmentAfter(node, ms) {
1688
+ const range = document.createRange();
1689
+ range.setStartBefore(ms);
1690
+ range.setEndAfter(node);
1691
+
1692
+ return range.cloneContents();
1693
+ }
1694
+
1695
+ _updateSource(newVal, oldVal) {
1696
+ if (typeof oldVal !== 'undefined' && newVal !== oldVal) {
1697
+ this.xpath = null;
1698
+ this.odd = null;
1699
+ this.xmlId = null;
1700
+ this.nodeId = null;
1414
1701
  }
1702
+ }
1703
+
1704
+ static get styles() {
1705
+ return css`
1706
+ :host {
1707
+ display: block;
1708
+ background: transparent;
1709
+ }
1710
+
1711
+ :host(.noscroll) {
1712
+ scrollbar-width: none; /* Firefox 64 */
1713
+ -ms-overflow-style: none;
1714
+ }
1715
+
1716
+ :host(.noscroll)::-webkit-scrollbar {
1717
+ width: 0 !important;
1718
+ display: none;
1719
+ }
1720
+
1721
+ [id] {
1722
+ scroll-margin-top: var(--pb-view-scroll-margin-top);
1723
+ }
1724
+
1725
+ #view {
1726
+ position: relative;
1727
+ font-size: clamp(
1728
+ calc(var(--pb-content-font-size, 1rem) * var(--pb-min-zoom, 0.5)),
1729
+ calc(var(--pb-content-font-size, 1rem) * var(--pb-zoom-factor)),
1730
+ calc(var(--pb-content-font-size, 1rem) * var(--pb-max-zoom, 3))
1731
+ );
1732
+ line-height: calc(var(--pb-content-line-height, 1.5) * var(--pb-zoom-factor));
1733
+ }
1734
+
1735
+ .columns {
1736
+ display: grid;
1737
+ grid-template-columns: calc(50% - var(--pb-view-column-gap, 10px) / 2) calc(
1738
+ 50% - var(--pb-view-column-gap, 10px) / 2
1739
+ );
1740
+ grid-column-gap: var(--pb-view-column-gap, 10px);
1741
+ }
1742
+
1743
+ .margin-note {
1744
+ display: none;
1745
+ }
1746
+
1747
+ @media (min-width: 769px) {
1748
+ .content.margin-right {
1749
+ margin-right: 200px;
1750
+ }
1751
+
1752
+ .margin-note {
1753
+ background: rgba(153, 153, 153, 0.2);
1754
+ display: block;
1755
+ font-size: small;
1756
+ margin-right: -200px;
1757
+ margin-bottom: 5px;
1758
+ padding: 5px 0;
1759
+ float: right;
1760
+ clear: both;
1761
+ width: 180px;
1762
+ }
1763
+
1764
+ .margin-note .n {
1765
+ color: #777777;
1766
+ }
1767
+ }
1768
+
1769
+ a[rel='footnote'] {
1770
+ font-size: calc(
1771
+ var(--pb-footnote-font-size, var(--pb-content-font-size, 75%)) * var(--pb-zoom-factor, 1)
1772
+ );
1773
+ font-family: var(--pb-footnote-font-family, --pb-content-font-family);
1774
+ vertical-align: super;
1775
+ color: var(--pb-footnote-color, var(--pb-color-primary, #333333));
1776
+ text-decoration: none;
1777
+ padding: var(--pb-footnote-padding, 0 0 0 0.25em);
1778
+ line-height: 1;
1779
+ }
1780
+
1781
+ .list dt {
1782
+ float: left;
1783
+ }
1784
+
1785
+ .footnote .fn-number {
1786
+ float: left;
1787
+ font-size: var(--pb-footnote-font-size, var(--pb-content-font-size, 75%));
1788
+ }
1789
+
1790
+ .observer {
1791
+ display: block;
1792
+ width: 100%;
1793
+ height: var(--pb-view-loader-height, 16px);
1794
+ font-family: var(--pb-view-loader-font, --pb-base-font);
1795
+ color: var(--pb-view-loader-color, black);
1796
+ background: var(--pb-view-loader-background, #909090);
1797
+ background-image: var(
1798
+ --pb-view-loader-background-image,
1799
+ repeating-linear-gradient(
1800
+ 45deg,
1801
+ transparent,
1802
+ transparent 35px,
1803
+ rgba(255, 255, 255, 0.5) 35px,
1804
+ rgba(255, 255, 255, 0.5) 70px
1805
+ )
1806
+ );
1807
+ animation-name: loader;
1808
+ animation-timing-function: linear;
1809
+ animation-duration: 2s;
1810
+ animation-fill-mode: forwards;
1811
+ animation-iteration-count: infinite;
1812
+ }
1813
+
1814
+ @keyframes loader {
1815
+ 0% {
1816
+ background-position: 3rem 0;
1817
+ }
1818
+
1819
+ 100% {
1820
+ background-position: 0 0;
1821
+ }
1822
+ }
1823
+
1824
+ .scroll-fragment {
1825
+ animation: fadeIn ease 500ms;
1826
+ }
1827
+
1828
+ @keyframes fadeIn {
1829
+ 0% {
1830
+ opacity: 0;
1831
+ }
1832
+ 100% {
1833
+ opacity: 1;
1834
+ }
1835
+ }
1836
+ `;
1837
+ }
1838
+
1839
+ render() {
1840
+ return [
1841
+ html`
1842
+ <div id="view" part="content">
1843
+ ${this._style}
1844
+ ${this.infiniteScroll ? html`<div id="top-observer" class="observer"></div>` : null}
1845
+ <div class="columns">
1846
+ <div id="column1">${this._column1}</div>
1847
+ <div id="column2">${this._column2}</div>
1848
+ </div>
1849
+ <div id="content">${this._content}</div>
1850
+ ${this.infiniteScroll ? html`<div id="bottom-observer" class="observer"></div>` : null}
1851
+ <div id="footnotes" part="footnotes">${this._footnotes}</div>
1852
+ </div>
1853
+ <pb-fetch
1854
+ id="loadContent"
1855
+ verbose
1856
+ handle-as="json"
1857
+ method="get"
1858
+ with-credentials
1859
+ @response="${this._handleContent}"
1860
+ @error="${this._handleError}"
1861
+ ></pb-fetch>
1862
+ `,
1863
+ ];
1864
+ }
1415
1865
  }
1416
1866
 
1417
1867
  customElements.define('pb-view', PbView);