@canopy-iiif/app 1.10.0 → 1.10.3

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.
@@ -1,5 +1,9 @@
1
1
  import { mountImageStory } from '../../ui/dist/index.mjs';
2
2
 
3
+ const IMAGE_STORY_SELECTOR = '[data-canopy-image-story]';
4
+ const nodeStates = new WeakMap();
5
+ const SIZE_EPSILON = 1;
6
+
3
7
  function ready(fn) {
4
8
  if (typeof document === 'undefined') return;
5
9
  if (document.readyState === 'loading') {
@@ -20,19 +24,161 @@ function parseProps(node) {
20
24
  }
21
25
  }
22
26
 
27
+ function getNodeState(node) {
28
+ if (!node) return null;
29
+ let state = nodeStates.get(node);
30
+ if (!state) {
31
+ state = {
32
+ mounted: false,
33
+ cleanup: null,
34
+ resizeObserver: null,
35
+ pollId: null,
36
+ watching: false,
37
+ props: null,
38
+ lastSize: null,
39
+ };
40
+ nodeStates.set(node, state);
41
+ }
42
+ return state;
43
+ }
44
+
45
+ function disconnectWatchers(state) {
46
+ if (!state) return;
47
+ if (state.resizeObserver) {
48
+ try {
49
+ state.resizeObserver.disconnect();
50
+ } catch (_) {}
51
+ state.resizeObserver = null;
52
+ }
53
+ if (state.pollId && typeof window !== 'undefined') {
54
+ window.clearTimeout(state.pollId);
55
+ state.pollId = null;
56
+ }
57
+ state.watching = false;
58
+ }
59
+
60
+ function destroyNode(node, state) {
61
+ const currentState = state || getNodeState(node);
62
+ if (!currentState) return;
63
+ disconnectWatchers(currentState);
64
+ if (currentState.cleanup) {
65
+ try {
66
+ currentState.cleanup();
67
+ } catch (_) {}
68
+ currentState.cleanup = null;
69
+ }
70
+ currentState.mounted = false;
71
+ }
72
+
73
+ function measureSize(node) {
74
+ if (!node) return null;
75
+ const rect = node.getBoundingClientRect();
76
+ const width = rect?.width || node.offsetWidth || node.clientWidth || 0;
77
+ const height = rect?.height || node.offsetHeight || node.clientHeight || 0;
78
+ return { width, height };
79
+ }
80
+
81
+ function hasUsableSize(node, state) {
82
+ const size = measureSize(node);
83
+ if (!size) return false;
84
+ const usable = size.width > 2 && size.height > 2;
85
+ if (usable && state) {
86
+ state.lastSize = size;
87
+ }
88
+ return usable;
89
+ }
90
+
91
+ function needsSizeRefresh(node, state) {
92
+ if (!node || !state) return false;
93
+ const size = measureSize(node);
94
+ if (!size) return false;
95
+ if (size.width <= 2 || size.height <= 2) {
96
+ return true;
97
+ }
98
+ if (!state.lastSize) {
99
+ state.lastSize = size;
100
+ return true;
101
+ }
102
+ const widthDelta = Math.abs(size.width - state.lastSize.width);
103
+ const heightDelta = Math.abs(size.height - state.lastSize.height);
104
+ if (widthDelta > SIZE_EPSILON || heightDelta > SIZE_EPSILON) {
105
+ state.lastSize = size;
106
+ return true;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ function attemptMount(node, state) {
112
+ if (!node || !state || state.mounted) return false;
113
+ if (!hasUsableSize(node, state)) return false;
114
+ state.mounted = true;
115
+ disconnectWatchers(state);
116
+ const props = state.props || parseProps(node);
117
+ Promise.resolve(mountImageStory(node, props)).then((destroy) => {
118
+ if (typeof destroy === 'function') {
119
+ state.cleanup = destroy;
120
+ } else {
121
+ state.cleanup = null;
122
+ }
123
+ });
124
+ return true;
125
+ }
126
+
127
+ function scheduleWatchers(node, state, tryMount) {
128
+ if (!node || !state || state.watching) return;
129
+ if (typeof window === 'undefined') return;
130
+ state.watching = true;
131
+ if (typeof window !== 'undefined' && typeof window.ResizeObserver === 'function') {
132
+ state.resizeObserver = new window.ResizeObserver(() => {
133
+ if (state.mounted) return;
134
+ tryMount();
135
+ });
136
+ try {
137
+ state.resizeObserver.observe(node);
138
+ } catch (_) {}
139
+ }
140
+ const schedulePoll = () => {
141
+ if (state.mounted) return;
142
+ state.pollId = window.setTimeout(() => {
143
+ state.pollId = null;
144
+ if (!tryMount()) {
145
+ schedulePoll();
146
+ }
147
+ }, 200);
148
+ };
149
+ schedulePoll();
150
+ }
151
+
152
+ function startMountProcess(node) {
153
+ const state = getNodeState(node);
154
+ if (!state) return;
155
+ state.props = parseProps(node);
156
+ const tryMount = () => attemptMount(node, state);
157
+ if (!tryMount()) {
158
+ scheduleWatchers(node, state, tryMount);
159
+ }
160
+ }
161
+
23
162
  function mount(node) {
24
- if (!node || node.__canopyImageStoryMounted) return;
25
- try {
26
- const props = parseProps(node);
27
- mountImageStory(node, props);
28
- node.__canopyImageStoryMounted = true;
29
- } catch (_) {}
163
+ if (!node) return;
164
+ const state = getNodeState(node);
165
+ if (!state || state.bound) return;
166
+ state.bound = true;
167
+ startMountProcess(node);
168
+ }
169
+
170
+ function remount(node) {
171
+ const state = getNodeState(node);
172
+ if (!state) return;
173
+ state.props = parseProps(node);
174
+ destroyNode(node, state);
175
+ startMountProcess(node);
30
176
  }
31
177
 
32
178
  function scan() {
33
179
  try {
34
180
  document
35
- .querySelectorAll('[data-canopy-image-story]')
181
+ .querySelectorAll(IMAGE_STORY_SELECTOR)
36
182
  .forEach((node) => mount(node));
37
183
  } catch (_) {}
38
184
  }
@@ -45,11 +191,11 @@ function observe() {
45
191
  mutation.addedNodes &&
46
192
  mutation.addedNodes.forEach((node) => {
47
193
  if (!(node instanceof Element)) return;
48
- if (node.matches && node.matches('[data-canopy-image-story]')) {
194
+ if (node.matches && node.matches(IMAGE_STORY_SELECTOR)) {
49
195
  toMount.push(node);
50
196
  }
51
197
  const inner = node.querySelectorAll
52
- ? node.querySelectorAll('[data-canopy-image-story]')
198
+ ? node.querySelectorAll(IMAGE_STORY_SELECTOR)
53
199
  : [];
54
200
  inner && inner.forEach && inner.forEach((el) => toMount.push(el));
55
201
  });
@@ -63,7 +209,41 @@ function observe() {
63
209
  } catch (_) {}
64
210
  }
65
211
 
212
+ function refreshModal(modal) {
213
+ if (!modal || typeof modal.querySelectorAll !== 'function') return;
214
+ const nodes = modal.querySelectorAll(IMAGE_STORY_SELECTOR);
215
+ if (!nodes || !nodes.length) return;
216
+ Array.prototype.forEach.call(nodes, (node) => {
217
+ const state = getNodeState(node);
218
+ if (!state || !state.mounted) return;
219
+ if (needsSizeRefresh(node, state)) {
220
+ remount(node);
221
+ }
222
+ });
223
+ }
224
+
225
+ function handleGalleryModalChange(event) {
226
+ if (!event || typeof document === 'undefined') return;
227
+ const detail = event.detail || {};
228
+ if (detail.state !== 'open') return;
229
+ let modal = detail.modal;
230
+ if (!modal && detail.modalId) {
231
+ modal = document.getElementById(detail.modalId);
232
+ }
233
+ if (modal) {
234
+ refreshModal(modal);
235
+ }
236
+ }
237
+
238
+ function bindGalleryListener() {
239
+ if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') {
240
+ return;
241
+ }
242
+ window.addEventListener('canopy:gallery:modal-change', handleGalleryModalChange);
243
+ }
244
+
66
245
  ready(function onReady() {
67
246
  scan();
68
247
  observe();
248
+ bindGalleryListener();
69
249
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-iiif/app",
3
- "version": "1.10.0",
3
+ "version": "1.10.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "author": "Mat Jordan <mat@northwestern.edu>",
package/ui/dist/index.mjs CHANGED
@@ -45483,6 +45483,7 @@ async function mountImageStory(element, props = {}) {
45483
45483
  // ui/src/iiif/ImageStory.jsx
45484
45484
  var DEFAULT_IMAGE_STORY_HEIGHT = 600;
45485
45485
  var NUMERIC_HEIGHT_PATTERN = /^[+-]?(?:\d+|\d*\.\d+)$/;
45486
+ var SIZE_EPSILON = 1;
45486
45487
  function resolveContainerHeight(value) {
45487
45488
  if (typeof value === "number" && Number.isFinite(value)) {
45488
45489
  return `${value}px`;
@@ -45533,6 +45534,7 @@ var ImageStory = (props = {}) => {
45533
45534
  let mounted = false;
45534
45535
  let resizeObserver = null;
45535
45536
  let pollId = null;
45537
+ let lastKnownSize = null;
45536
45538
  const payload = sanitizeImageStoryProps({
45537
45539
  iiifContent,
45538
45540
  disablePanAndZoom,
@@ -45561,12 +45563,39 @@ var ImageStory = (props = {}) => {
45561
45563
  pollId = null;
45562
45564
  }
45563
45565
  };
45564
- const hasUsableSize = () => {
45565
- if (!node) return false;
45566
+ const measureSize = () => {
45567
+ if (!node) return null;
45566
45568
  const rect = node.getBoundingClientRect();
45567
- const width = (rect == null ? void 0 : rect.width) || node.offsetWidth || node.clientWidth;
45568
- const height2 = (rect == null ? void 0 : rect.height) || node.offsetHeight || node.clientHeight;
45569
- return width > 2 && height2 > 2;
45569
+ const width = (rect == null ? void 0 : rect.width) || node.offsetWidth || node.clientWidth || 0;
45570
+ const height2 = (rect == null ? void 0 : rect.height) || node.offsetHeight || node.clientHeight || 0;
45571
+ return { width, height: height2 };
45572
+ };
45573
+ const hasUsableSize = () => {
45574
+ const size = measureSize();
45575
+ if (!size) return false;
45576
+ const usable = size.width > 2 && size.height > 2;
45577
+ if (usable) {
45578
+ lastKnownSize = size;
45579
+ }
45580
+ return usable;
45581
+ };
45582
+ const hasMeaningfulSizeChange = () => {
45583
+ const size = measureSize();
45584
+ if (!size) return false;
45585
+ if (size.width <= 2 || size.height <= 2) {
45586
+ return true;
45587
+ }
45588
+ if (!lastKnownSize) {
45589
+ lastKnownSize = size;
45590
+ return true;
45591
+ }
45592
+ const widthDelta = Math.abs(size.width - lastKnownSize.width);
45593
+ const heightDelta = Math.abs(size.height - lastKnownSize.height);
45594
+ if (widthDelta > SIZE_EPSILON || heightDelta > SIZE_EPSILON) {
45595
+ lastKnownSize = size;
45596
+ return true;
45597
+ }
45598
+ return false;
45570
45599
  };
45571
45600
  const mountViewer = () => {
45572
45601
  if (!node || mounted || cancelled) return false;
@@ -45582,8 +45611,9 @@ var ImageStory = (props = {}) => {
45582
45611
  });
45583
45612
  return true;
45584
45613
  };
45585
- if (!mountViewer()) {
45586
- if (typeof window !== "undefined" && typeof window.ResizeObserver === "function") {
45614
+ const scheduleWatchers = () => {
45615
+ if (mounted || cancelled) return;
45616
+ if (!resizeObserver && typeof window !== "undefined" && typeof window.ResizeObserver === "function") {
45587
45617
  resizeObserver = new window.ResizeObserver(() => {
45588
45618
  if (mounted || cancelled) return;
45589
45619
  mountViewer();
@@ -45602,12 +45632,48 @@ var ImageStory = (props = {}) => {
45602
45632
  }
45603
45633
  }, 200);
45604
45634
  };
45605
- schedulePoll();
45635
+ if (!pollId) {
45636
+ schedulePoll();
45637
+ }
45638
+ };
45639
+ const beginMounting = () => {
45640
+ if (!mountViewer()) {
45641
+ scheduleWatchers();
45642
+ }
45643
+ };
45644
+ const remountViewer = () => {
45645
+ if (cancelled) return;
45646
+ if (mounted) {
45647
+ mounted = false;
45648
+ destroyCleanup();
45649
+ }
45650
+ beginMounting();
45651
+ };
45652
+ beginMounting();
45653
+ const handleGalleryModalChange = (event) => {
45654
+ if (!node || !event || typeof document === "undefined") return;
45655
+ const detail = event.detail || {};
45656
+ if (detail.state !== "open") return;
45657
+ const modal = detail.modal || (detail.modalId ? document.getElementById(detail.modalId) : null);
45658
+ if (!modal || !modal.contains(node)) return;
45659
+ if (!mounted) return;
45660
+ if (hasMeaningfulSizeChange()) {
45661
+ remountViewer();
45662
+ }
45663
+ };
45664
+ if (typeof window !== "undefined" && window.addEventListener) {
45665
+ window.addEventListener("canopy:gallery:modal-change", handleGalleryModalChange);
45606
45666
  }
45607
45667
  return () => {
45608
45668
  cancelled = true;
45609
45669
  disconnectWatchers();
45610
45670
  destroyCleanup();
45671
+ if (typeof window !== "undefined" && window.removeEventListener) {
45672
+ window.removeEventListener(
45673
+ "canopy:gallery:modal-change",
45674
+ handleGalleryModalChange
45675
+ );
45676
+ }
45611
45677
  };
45612
45678
  }, [iiifContent, disablePanAndZoom, pointOfInterestSvgUrl, viewerOptions]);
45613
45679
  return /* @__PURE__ */ React27.createElement(
@@ -46481,6 +46547,15 @@ function ReferencedManifestCard({
46481
46547
  var DAY_MS = 24 * 60 * 60 * 1e3;
46482
46548
  var DEFAULT_TRACK_HEIGHT = 640;
46483
46549
  var MIN_HEIGHT_PER_POINT = 220;
46550
+ var SCALE_MODES = {
46551
+ TIME: "time",
46552
+ UNIFORM: "uniform"
46553
+ };
46554
+ var ALIGN_OPTIONS = {
46555
+ CENTER: "center",
46556
+ LEFT: "left",
46557
+ RIGHT: "right"
46558
+ };
46484
46559
  function getThresholdMs(threshold, granularity) {
46485
46560
  const value = Number(threshold);
46486
46561
  if (!Number.isFinite(value) || value <= 0) return 0;
@@ -46670,6 +46745,8 @@ function Timeline({
46670
46745
  locale: localeProp = "en-US",
46671
46746
  height = DEFAULT_TRACK_HEIGHT,
46672
46747
  threshold: thresholdProp = null,
46748
+ scale: scale2 = SCALE_MODES.TIME,
46749
+ align = ALIGN_OPTIONS.CENTER,
46673
46750
  steps = null,
46674
46751
  points: pointsProp,
46675
46752
  __canopyTimeline: payload = null,
@@ -46704,20 +46781,24 @@ function Timeline({
46704
46781
  );
46705
46782
  const spanStart = effectiveRange.startDate.getTime();
46706
46783
  const span = effectiveRange.span;
46784
+ const scaleValue = scale2 === SCALE_MODES.UNIFORM ? SCALE_MODES.UNIFORM : SCALE_MODES.TIME;
46785
+ const useUniformSpacing = scaleValue === SCALE_MODES.UNIFORM;
46786
+ const alignValue = align === ALIGN_OPTIONS.LEFT ? ALIGN_OPTIONS.LEFT : align === ALIGN_OPTIONS.RIGHT ? ALIGN_OPTIONS.RIGHT : ALIGN_OPTIONS.CENTER;
46787
+ const enforcedSide = alignValue === ALIGN_OPTIONS.LEFT ? "right" : alignValue === ALIGN_OPTIONS.RIGHT ? "left" : null;
46707
46788
  const pointsWithPosition = React41.useMemo(() => {
46708
46789
  if (!sanitizedPoints.length) return [];
46709
46790
  return sanitizedPoints.map((point2, index) => {
46710
46791
  const timestamp = point2.meta.timestamp;
46711
46792
  const fallbackProgress = sanitizedPoints.length > 1 ? index / (sanitizedPoints.length - 1) : 0;
46712
- const progress = Number.isFinite(timestamp) ? clampProgress((timestamp - spanStart) / span) : fallbackProgress;
46713
- const side = point2.side || (index % 2 === 0 ? "left" : "right");
46793
+ const progress = useUniformSpacing ? fallbackProgress : Number.isFinite(timestamp) ? clampProgress((timestamp - spanStart) / span) : fallbackProgress;
46794
+ const side = enforcedSide || point2.side || (index % 2 === 0 ? "left" : "right");
46714
46795
  return {
46715
46796
  ...point2,
46716
46797
  progress,
46717
46798
  side
46718
46799
  };
46719
46800
  });
46720
- }, [sanitizedPoints, spanStart, span]);
46801
+ }, [sanitizedPoints, spanStart, span, useUniformSpacing, enforcedSide]);
46721
46802
  const [activeId, setActiveId] = React41.useState(
46722
46803
  () => getActivePointId(pointsWithPosition)
46723
46804
  );
@@ -46764,7 +46845,11 @@ function Timeline({
46764
46845
  });
46765
46846
  }, []);
46766
46847
  const trackHeight = resolveTrackHeight(height, pointsWithPosition.length);
46767
- const containerClasses = ["canopy-timeline", className].filter(Boolean).join(" ");
46848
+ const containerClasses = [
46849
+ "canopy-timeline",
46850
+ alignValue ? `canopy-timeline--align-${alignValue}` : "",
46851
+ className
46852
+ ].filter(Boolean).join(" ");
46768
46853
  const rangeLabel = formatRangeLabel(effectiveRange);
46769
46854
  function renderPointEntry(point2) {
46770
46855
  if (!point2) return null;
@@ -48194,6 +48279,23 @@ var INLINE_SCRIPT = `(() => {
48194
48279
  const NAV_OPTION_SELECTOR = '[data-canopy-gallery-nav-option]';
48195
48280
  const NAV_ITEM_SELECTOR = '[data-canopy-gallery-nav-item]';
48196
48281
 
48282
+ function emitModalState(modal, state) {
48283
+ if (!modal || typeof window === 'undefined') return;
48284
+ const detail = { modalId: modal.id || '', modal, state };
48285
+ try {
48286
+ const EventCtor = window.CustomEvent || CustomEvent;
48287
+ if (typeof EventCtor === 'function') {
48288
+ window.dispatchEvent(new EventCtor('canopy:gallery:modal-change', { detail }));
48289
+ return;
48290
+ }
48291
+ } catch (_) {}
48292
+ try {
48293
+ const fallback = document.createEvent('CustomEvent');
48294
+ fallback.initCustomEvent('canopy:gallery:modal-change', true, true, detail);
48295
+ window.dispatchEvent(fallback);
48296
+ } catch (_) {}
48297
+ }
48298
+
48197
48299
  function isVisible(node) {
48198
48300
  return !!(node && (node.offsetWidth || node.offsetHeight || node.getClientRects().length));
48199
48301
  }
@@ -48324,6 +48426,7 @@ var INLINE_SCRIPT = `(() => {
48324
48426
  lockScroll();
48325
48427
  document.addEventListener('keydown', handleKeydown, true);
48326
48428
  } else if (activeModal !== modal) {
48429
+ emitModalState(activeModal, 'close');
48327
48430
  activeModal.removeAttribute('data-canopy-gallery-active');
48328
48431
  }
48329
48432
  activeModal = modal;
@@ -48332,6 +48435,7 @@ var INLINE_SCRIPT = `(() => {
48332
48435
  if (!focusActiveNav(modal)) {
48333
48436
  focusInitial(modal);
48334
48437
  }
48438
+ emitModalState(modal, 'open');
48335
48439
  return;
48336
48440
  }
48337
48441
  if (!activeModal) return;
@@ -48365,6 +48469,7 @@ var INLINE_SCRIPT = `(() => {
48365
48469
  }
48366
48470
  });
48367
48471
  }
48472
+ emitModalState(previous, 'close');
48368
48473
  }
48369
48474
 
48370
48475
  function modalFromHash() {
@@ -48823,6 +48928,7 @@ function buildCaptionContent(itemProps) {
48823
48928
  ));
48824
48929
  }
48825
48930
  function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
48931
+ const { getString, formatString } = useLocale();
48826
48932
  const {
48827
48933
  props,
48828
48934
  modalId,
@@ -48835,6 +48941,12 @@ function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
48835
48941
  const kicker = props.kicker || props.label || props.eyebrow;
48836
48942
  const summary = props.popupDescription || props.modalDescription || props.description || props.summary || null;
48837
48943
  const modalTitle = props.popupTitle || props.modalTitle || props.title || `Item ${index + 1}`;
48944
+ const closeButtonText = getString("common.actions.close", "Close");
48945
+ const closeButtonLabel = formatString(
48946
+ "common.phrases.close_content",
48947
+ "Close {content}",
48948
+ { content: modalTitle }
48949
+ );
48838
48950
  return /* @__PURE__ */ React44.createElement(
48839
48951
  "div",
48840
48952
  {
@@ -48856,13 +48968,14 @@ function GalleryModal({ item, closeTargetId, navItems, navGroupName }) {
48856
48968
  groupName: `${navGroupName || "canopy-gallery"}-${modalId}`
48857
48969
  }
48858
48970
  ), /* @__PURE__ */ React44.createElement(
48859
- "a",
48971
+ Button,
48860
48972
  {
48861
48973
  className: "canopy-gallery__modal-close",
48862
48974
  href: `#${closeTargetId}`,
48863
- "aria-label": `Close popup for ${modalTitle}`
48864
- },
48865
- "X"
48975
+ label: closeButtonText,
48976
+ "aria-label": closeButtonLabel,
48977
+ variant: "secondary"
48978
+ }
48866
48979
  )), /* @__PURE__ */ React44.createElement("div", { className: "canopy-gallery__modal-panel" }, /* @__PURE__ */ React44.createElement(
48867
48980
  "button",
48868
48981
  {