@teipublisher/pb-components 3.3.1 → 3.4.0

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.
@@ -342,6 +342,10 @@ class PbViewAnnotate extends PbView {
342
342
 
343
343
  this.subscribeTo('pb-add-annotation', ev => this.addAnnotation(ev.detail));
344
344
  this.subscribeTo('pb-edit-annotation', this._editAnnotation.bind(this));
345
+ this.subscribeTo('pb-start-update', () => {
346
+ this._cancelMarkerRefresh();
347
+ this._clearMarkers();
348
+ });
345
349
  this.subscribeTo('pb-refresh', () => {
346
350
  this._ranges = [];
347
351
  this._rangesMap.clear();
@@ -373,6 +377,7 @@ class PbViewAnnotate extends PbView {
373
377
  disconnectedCallback() {
374
378
  super.disconnectedCallback();
375
379
 
380
+ this._cancelMarkerRefresh();
376
381
  // Remove any events that we had earlier.
377
382
  this._disconnectedSignal.abort();
378
383
  }
@@ -383,10 +388,11 @@ class PbViewAnnotate extends PbView {
383
388
 
384
389
  set annotations(annoData) {
385
390
  this._ranges = annoData;
386
- this.updateAnnotations(true);
391
+ this.updateAnnotations(true, false);
387
392
  this._markIncompleteAnnotations();
388
393
  this._initAnnotationColors();
389
394
  this._annotationStyles();
395
+ this._scheduleMarkerRefresh();
390
396
  }
391
397
 
392
398
  saveHistory() {
@@ -431,7 +437,7 @@ class PbViewAnnotate extends PbView {
431
437
 
432
438
  zoom(direction) {
433
439
  super.zoom(direction);
434
- window.requestAnimationFrame(() => this.refreshMarkers());
440
+ this._scheduleMarkerRefresh();
435
441
  }
436
442
 
437
443
  getKey(type) {
@@ -444,7 +450,7 @@ class PbViewAnnotate extends PbView {
444
450
  const scheduleCallback = () => {
445
451
  _pendingCallback = setTimeout(() => {
446
452
  _pendingCallback = null;
447
- this.refreshMarkers();
453
+ this._scheduleMarkerRefresh();
448
454
  }, 200);
449
455
  };
450
456
  window.addEventListener('resize', () => {
@@ -467,19 +473,76 @@ class PbViewAnnotate extends PbView {
467
473
 
468
474
  _handleContent() {
469
475
  super._handleContent();
470
- this.updateComplete.then(() =>
471
- setTimeout(() => {
472
- this._initAnnotationColors();
473
- this._annotationStyles();
474
- this.updateAnnotations();
475
- this._markIncompleteAnnotations();
476
- if (this._scrollTop) {
477
- this.scrollTop = this._scrollTop;
478
- this._scrollTop = undefined;
479
- }
480
- this.emitTo('pb-annotations-loaded');
481
- }, 300),
482
- );
476
+ this.updateComplete.then(() => {
477
+ this._initAnnotationColors();
478
+ this._annotationStyles();
479
+ this.updateAnnotations(true, false);
480
+ this._markIncompleteAnnotations();
481
+ if (this._scrollTop) {
482
+ this.scrollTop = this._scrollTop;
483
+ this._scrollTop = undefined;
484
+ }
485
+ this.emitTo('pb-annotations-loaded');
486
+ // Marker positions depend on the ODD stylesheet and final text layout.
487
+ this._scheduleMarkerRefresh();
488
+ });
489
+ }
490
+
491
+ _cancelMarkerRefresh() {
492
+ if (this._markerRefreshController) {
493
+ this._markerRefreshController.abort();
494
+ this._markerRefreshController = null;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Refresh annotation markers after layout has settled. Measuring too early
500
+ * (e.g. while paginated content is still being replaced) leaves markers at
501
+ * stale coordinates.
502
+ */
503
+ async _scheduleMarkerRefresh() {
504
+ this._cancelMarkerRefresh();
505
+ const controller = new AbortController();
506
+ this._markerRefreshController = controller;
507
+ const { signal } = controller;
508
+
509
+ try {
510
+ await this.updateComplete;
511
+ if (signal.aborted) {
512
+ return;
513
+ }
514
+
515
+ const styleLink = this.shadowRoot.querySelector('#view link[rel="stylesheet"]');
516
+ if (styleLink && !styleLink.sheet) {
517
+ await new Promise(resolve => {
518
+ const done = () => resolve();
519
+ styleLink.addEventListener('load', done, { once: true });
520
+ styleLink.addEventListener('error', done, { once: true });
521
+ });
522
+ }
523
+ if (signal.aborted) {
524
+ return;
525
+ }
526
+
527
+ if (document.fonts?.ready) {
528
+ await document.fonts.ready;
529
+ }
530
+ if (signal.aborted) {
531
+ return;
532
+ }
533
+
534
+ await new Promise(resolve => requestAnimationFrame(resolve));
535
+ await new Promise(resolve => requestAnimationFrame(resolve));
536
+ if (signal.aborted) {
537
+ return;
538
+ }
539
+
540
+ this._paintMarkers();
541
+ } finally {
542
+ if (this._markerRefreshController === controller) {
543
+ this._markerRefreshController = null;
544
+ }
545
+ }
483
546
  }
484
547
 
485
548
  _updateAnnotation(teiRange, silent = false, batch = false) {
@@ -548,13 +611,13 @@ class PbViewAnnotate extends PbView {
548
611
  this._rangesMap.set(span, teiRange);
549
612
 
550
613
  if (!batch) {
551
- this.refreshMarkers();
614
+ this._scheduleMarkerRefresh();
552
615
  }
553
616
 
554
617
  return span;
555
618
  }
556
619
 
557
- updateAnnotations(silent = false) {
620
+ updateAnnotations(silent = false, refreshMarkers = true) {
558
621
  this._ranges.forEach(teiRange => {
559
622
  let span;
560
623
  switch (teiRange.type) {
@@ -579,7 +642,9 @@ class PbViewAnnotate extends PbView {
579
642
  break;
580
643
  }
581
644
  });
582
- window.requestAnimationFrame(() => this.refreshMarkers());
645
+ if (refreshMarkers) {
646
+ this._scheduleMarkerRefresh();
647
+ }
583
648
  }
584
649
 
585
650
  _getSelection() {
@@ -750,7 +815,7 @@ class PbViewAnnotate extends PbView {
750
815
 
751
816
  this.emitTo('pb-annotations-changed', { ranges: this._ranges });
752
817
 
753
- window.requestAnimationFrame(() => this.refreshMarkers());
818
+ this._scheduleMarkerRefresh();
754
819
 
755
820
  this._inHandler = true;
756
821
  try {
@@ -976,11 +1041,16 @@ class PbViewAnnotate extends PbView {
976
1041
 
977
1042
  /**
978
1043
  * For all annotations currently shown, create a marker element and position
979
- * it absolute next to the annotation
980
- *
981
- * @param {HTMLElement} root element containing the markers
1044
+ * it absolute next to the annotation. Waits for layout to settle before measuring.
982
1045
  */
983
1046
  refreshMarkers() {
1047
+ this._scheduleMarkerRefresh();
1048
+ }
1049
+
1050
+ /**
1051
+ * Measure annotation spans and paint underline markers into the marker layer.
1052
+ */
1053
+ _paintMarkers() {
984
1054
  const root = this.shadowRoot.getElementById('view');
985
1055
  const rootRect = root.getBoundingClientRect();
986
1056
  const markerLayer = this.shadowRoot.getElementById('marker-layer');
@@ -1212,6 +1282,19 @@ class PbViewAnnotate extends PbView {
1212
1282
  return [
1213
1283
  super.styles,
1214
1284
  css`
1285
+ :host {
1286
+ position: relative;
1287
+ }
1288
+
1289
+ #marker-layer {
1290
+ position: absolute;
1291
+ top: 0;
1292
+ left: 0;
1293
+ width: 100%;
1294
+ height: 100%;
1295
+ pointer-events: none;
1296
+ }
1297
+
1215
1298
  .annotation-type {
1216
1299
  display: inline-block;
1217
1300
  text-align: right;
package/src/pb-view.js CHANGED
@@ -650,6 +650,18 @@ export class PbView extends themableMixin(pbMixin(LitElement)) {
650
650
  _doLoad(params) {
651
651
  this.emitTo('pb-start-update', params);
652
652
 
653
+ // On the first load, check whether the server already rendered this
654
+ // fragment into our light DOM. If so, request only
655
+ // the navigation metadata (content=none) and adopt the existing markup
656
+ // instead of rendering the same content a second time.
657
+ if (this._ssrContent === undefined) {
658
+ this._ssrContent = this.querySelector(':scope > [data-pb-ssr]') || null;
659
+ this._ssrFootnotes = this.querySelector(':scope > [data-pb-ssr-footnotes]') || null;
660
+ }
661
+ if (this._ssrContent) {
662
+ params.content = 'none';
663
+ }
664
+
653
665
  console.log('<pb-view> Loading view with params %o', params);
654
666
  if (!this.infiniteScroll) {
655
667
  this._clear();
@@ -709,9 +721,7 @@ export class PbView extends themableMixin(pbMixin(LitElement)) {
709
721
 
710
722
  const index = await fetch(`index.json`).then(response => response.json());
711
723
  const paramNames = ['odd', 'view', 'xpath', 'map'];
712
- this.querySelectorAll('pb-param').forEach(param =>
713
- paramNames.push(`user.${param.getAttribute('name')}`),
714
- );
724
+ this._params().forEach(param => paramNames.push(`user.${param.getAttribute('name')}`));
715
725
  let url = params.id ? createKey([...paramNames, 'id']) : createKey([...paramNames, 'root']);
716
726
  let file = index[url];
717
727
  if (!file) {
@@ -827,7 +837,17 @@ export class PbView extends themableMixin(pbMixin(LitElement)) {
827
837
  const elem = document.createElement('div');
828
838
  // elem.style.opacity = 0; //hide it - animation has to make sure to blend it in
829
839
  fragment.appendChild(elem);
830
- elem.innerHTML = resp.content;
840
+ if (this._ssrContent && (!resp.content || resp.content.length === 0)) {
841
+ // Adopt the server-rendered content from light DOM (SSR) rather than
842
+ // re-rendering it: move its children into the shadow content container.
843
+ while (this._ssrContent.firstChild) {
844
+ elem.appendChild(this._ssrContent.firstChild);
845
+ }
846
+ this._ssrContent.remove();
847
+ this._ssrContent = null;
848
+ } else {
849
+ elem.innerHTML = resp.content;
850
+ }
831
851
 
832
852
  // if before-update-event is set, we do not replace the content immediately,
833
853
  // but emit an event
@@ -889,11 +909,23 @@ export class PbView extends themableMixin(pbMixin(LitElement)) {
889
909
 
890
910
  if (this.appendFootnotes) {
891
911
  const footnotes = document.createElement('div');
892
- if (resp.footnotes) {
912
+ if (this._ssrFootnotes) {
913
+ // Adopt the server-rendered footnotes from light DOM (SSR).
914
+ while (this._ssrFootnotes.firstChild) {
915
+ footnotes.appendChild(this._ssrFootnotes.firstChild);
916
+ }
917
+ } else if (resp.footnotes) {
893
918
  footnotes.innerHTML = resp.footnotes;
894
919
  }
895
920
  this._footnotes = footnotes;
896
921
  }
922
+ // Drop the SSR footnotes marker even when not appending, so it does not
923
+ // linger in the light DOM (matches the dynamic path, which ignores
924
+ // footnotes unless append-footnotes is set).
925
+ if (this._ssrFootnotes) {
926
+ this._ssrFootnotes.remove();
927
+ this._ssrFootnotes = null;
928
+ }
897
929
 
898
930
  this._initFootnotes(this._footnotes);
899
931
 
@@ -1013,9 +1045,20 @@ export class PbView extends themableMixin(pbMixin(LitElement)) {
1013
1045
  }
1014
1046
  }
1015
1047
 
1048
+ /**
1049
+ * Collect the configuration `pb-param` children of this view. Excludes any
1050
+ * `pb-param` that may occur inside server-rendered (SSR) content adopted into
1051
+ * the light DOM, so injected content can never be mistaken for a parameter.
1052
+ */
1053
+ _params() {
1054
+ return Array.from(this.querySelectorAll('pb-param')).filter(
1055
+ param => !param.closest('[data-pb-ssr], [data-pb-ssr-footnotes]'),
1056
+ );
1057
+ }
1058
+
1016
1059
  _getParameters() {
1017
1060
  const params = [];
1018
- this.querySelectorAll('pb-param').forEach(param => {
1061
+ this._params().forEach(param => {
1019
1062
  params[`user.${param.getAttribute('name')}`] = param.getAttribute('value');
1020
1063
  });
1021
1064
  // add parameters for features set with pb-toggle-feature