mirador-annotation-editor-video 1.1.5 → 1.1.6

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 (92) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/__tests__/AnnotationCreation.test.js +62 -28
  3. package/__tests__/AnnotationExportDialog.test.js +18 -16
  4. package/__tests__/CanvasListItem.test.js +53 -19
  5. package/__tests__/LocalStorageAdapter.test.js +1 -1
  6. package/__tests__/miradorAnnotationPlugin.test.js +97 -70
  7. package/__tests__/style-mock.js +1 -0
  8. package/__tests__/test-utils.js +57 -0
  9. package/demo/src/index.js +9 -4
  10. package/demo/src/quillConfig.js +34 -0
  11. package/es/AnnotationExportDialog.js +17 -25
  12. package/es/CanvasListItem.js +8 -4
  13. package/es/IIIFUtils.js +35 -3
  14. package/es/SingleCanvasDialog.js +1 -4
  15. package/es/TextEditor.js +9 -19
  16. package/es/annotationForm/AnnotationForm.js +14 -41
  17. package/es/annotationForm/AnnotationFormBody.js +36 -27
  18. package/es/annotationForm/AnnotationFormHeader.js +3 -3
  19. package/es/annotationForm/AnnotationFormOverlay/AnnotationDrawing.js +3 -1
  20. package/es/annotationForm/AnnotationFormOverlay/AnnotationFormOverlay.js +1 -1
  21. package/es/annotationForm/AnnotationFormOverlay/AnnotationFormOverlayTool.js +3 -2
  22. package/es/annotationForm/AnnotationFormOverlay/AnnotationFormOverlayToolOptions.js +8 -6
  23. package/es/annotationForm/AnnotationFormOverlay/KonvaDrawing/KonvaUtils.js +6 -20
  24. package/es/annotationForm/AnnotationFormOverlay/KonvaDrawing/shapes/ColorPicker.js +16 -6
  25. package/es/annotationForm/AnnotationFormTemplateSelector.js +9 -9
  26. package/es/annotationForm/AnnotationFormUtils.js +3 -0
  27. package/es/annotationForm/Debug.js +1 -0
  28. package/es/annotationForm/DebugInformation.js +27 -0
  29. package/es/annotationForm/MultiTagsInput.js +4 -1
  30. package/es/annotationForm/MultipleBodyTemplate.js +7 -9
  31. package/es/annotationForm/TaggingTemplate.js +0 -1
  32. package/es/annotationForm/TargetFormSection.js +4 -3
  33. package/es/annotationForm/TargetSpatialInput.js +4 -3
  34. package/es/annotationForm/TextCommentInput.js +25 -12
  35. package/es/annotationForm/TextCommentTemplate.js +0 -1
  36. package/es/annotationForm/UnsupportedMedia.js +43 -0
  37. package/es/containers/miradorAnnotationPlugin.js +1 -50
  38. package/es/custom.css +0 -13
  39. package/es/index.js +5 -12
  40. package/es/locales/locales.js +3 -2
  41. package/es/locales/locales_en.js +1 -1
  42. package/es/playerReferences.js +2 -1
  43. package/es/plugins/annotationCreationCompanionWindow.js +5 -8
  44. package/es/plugins/annotationSaga.js +44 -0
  45. package/es/plugins/canvasAnnotationsPlugin.js +86 -61
  46. package/es/plugins/canvasAnnotationsPluginUtils.js +202 -0
  47. package/es/plugins/externalStorageAnnotationPlugin.js +6 -71
  48. package/es/plugins/miradorAnnotationPlugin.js +44 -6
  49. package/es/plugins/windowSideBarButtonsPlugin.js +8 -10
  50. package/jest.config.js +14 -3
  51. package/package.json +8 -3
  52. package/setupJest.js +1 -4
  53. package/src/AnnotationExportDialog.js +12 -24
  54. package/src/CanvasListItem.js +4 -3
  55. package/src/IIIFUtils.js +33 -5
  56. package/src/SingleCanvasDialog.js +0 -3
  57. package/src/TextEditor.js +8 -32
  58. package/src/annotationForm/AnnotationForm.js +8 -47
  59. package/src/annotationForm/AnnotationFormBody.js +62 -83
  60. package/src/annotationForm/AnnotationFormHeader.js +3 -3
  61. package/src/annotationForm/AnnotationFormOverlay/AnnotationDrawing.js +3 -9
  62. package/src/annotationForm/AnnotationFormOverlay/AnnotationFormOverlay.js +1 -1
  63. package/src/annotationForm/AnnotationFormOverlay/AnnotationFormOverlayTool.js +2 -1
  64. package/src/annotationForm/AnnotationFormOverlay/AnnotationFormOverlayToolOptions.js +10 -6
  65. package/src/annotationForm/AnnotationFormOverlay/KonvaDrawing/KonvaUtils.js +14 -20
  66. package/src/annotationForm/AnnotationFormOverlay/KonvaDrawing/shapes/ColorPicker.js +14 -12
  67. package/src/annotationForm/AnnotationFormTemplateSelector.js +5 -7
  68. package/src/annotationForm/AnnotationFormUtils.js +2 -0
  69. package/src/annotationForm/Debug.js +0 -0
  70. package/src/annotationForm/DebugInformation.js +59 -0
  71. package/src/annotationForm/MultiTagsInput.js +4 -1
  72. package/src/annotationForm/MultipleBodyTemplate.js +7 -8
  73. package/src/annotationForm/TaggingTemplate.js +0 -1
  74. package/src/annotationForm/TargetFormSection.js +2 -3
  75. package/src/annotationForm/TargetSpatialInput.js +3 -3
  76. package/src/annotationForm/TextCommentInput.js +28 -14
  77. package/src/annotationForm/TextCommentTemplate.js +0 -1
  78. package/src/annotationForm/UnsupportedMedia.js +31 -0
  79. package/src/containers/miradorAnnotationPlugin.js +0 -36
  80. package/src/custom.css +0 -13
  81. package/src/index.js +10 -15
  82. package/src/locales/locales.js +3 -1
  83. package/src/locales/locales_en.js +1 -1
  84. package/src/playerReferences.js +5 -2
  85. package/src/plugins/annotationCreationCompanionWindow.js +9 -23
  86. package/src/plugins/annotationSaga.js +50 -0
  87. package/src/plugins/canvasAnnotationsPlugin.js +122 -98
  88. package/src/plugins/canvasAnnotationsPluginUtils.js +199 -0
  89. package/src/plugins/externalStorageAnnotationPlugin.js +6 -79
  90. package/src/plugins/miradorAnnotationPlugin.js +32 -4
  91. package/src/plugins/windowSideBarButtonsPlugin.js +6 -8
  92. package/webpack.config.js +1 -0
@@ -1,17 +1,33 @@
1
- import React, { useState } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useEffect, useMemo, useRef, useState,
4
+ } from 'react';
2
5
  import PropTypes from 'prop-types';
3
6
  import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
4
7
  import * as actions from 'mirador/dist/es/src/state/actions';
5
8
  import { getWindowViewType } from 'mirador/dist/es/src/state/selectors';
6
- import {
7
- getCompanionWindowsForContent,
8
- } from 'mirador/dist/es/src/state/selectors/companionWindows';
9
+ import { getCompanionWindowsForContent } from 'mirador/dist/es/src/state/selectors/companionWindows';
9
10
  import CanvasListItem from '../CanvasListItem';
10
11
  import AnnotationActionsContext from '../AnnotationActionsContext';
11
12
  import SingleCanvasDialog from '../SingleCanvasDialog';
12
13
  import translations from '../locales/locales';
14
+ import {
15
+ scrollToSelectedAnnotation,
16
+ } from './canvasAnnotationsPluginUtils';
13
17
 
14
- /** Functional Component */
18
+ /**
19
+ * CanvasAnnotationsWrapper
20
+ *
21
+ * Re-implements "scroll to selected annotation" inside the wrapper:
22
+ * - Observes selectedAnnotationId and scrolls the correct container so the <li> is visible.
23
+ * - Robust container resolution (ancestor/descendant/window).
24
+ * - Retries to survive focus/reflow resetting scrollTop.
25
+ *
26
+ * Props of interest:
27
+ * - targetProps.selectedAnnotationId: the currently selected annotation id.
28
+ * - scrollOffsetTop: px reserved for sticky header inside the scroller (default 96).
29
+ * - scrollRetries / scrollRetryDelay / scrollBehavior: tuning for robustness.
30
+ */
15
31
  function CanvasAnnotationsWrapper({
16
32
  addCompanionWindow,
17
33
  annotationsOnCanvases = {},
@@ -22,39 +38,77 @@ function CanvasAnnotationsWrapper({
22
38
  TargetComponent,
23
39
  targetProps,
24
40
  windowViewType,
25
- containerRef,
26
41
  annotationEditCompanionWindowIsOpened,
27
42
  t,
28
43
  }) {
29
44
  const [singleCanvasDialogOpen, setSingleCanvasDialogOpen] = useState(false);
30
45
 
31
- /** */
32
- const toggleSingleCanvasDialogOpen = () => {
33
- setSingleCanvasDialogOpen((prev) => !prev);
34
- };
46
+ const wrapperRef = useRef(null);
47
+ const bridgedScrollRef = useRef(null);
48
+
49
+ useEffect(() => {
50
+ const selId = targetProps?.selectedAnnotationId;
51
+ if (!selId) return;
52
+
53
+ const node = wrapperRef.current?.querySelector(`li[annotationid="${selId}"]`)
54
+ || wrapperRef.current?.querySelector('li.MuiMenuItem-root.Mui-selected');
55
+ if (!node) return;
56
+
57
+ scrollToSelectedAnnotation(node, bridgedScrollRef);
58
+ }, [
59
+ targetProps?.selectedAnnotationId,
60
+ ]);
61
+ /**
62
+ * Toggle the visibility state of the single canvas dialog.
63
+ *
64
+ * - Flips `singleCanvasDialogOpen` between `true` and `false`.
65
+ * - Used as the `handleClose` callback for the dialog and to open it.
66
+ *
67
+ * @function
68
+ * @returns {void}
69
+ */
70
+ const toggleSingleCanvasDialogOpen = useCallback(
71
+ () => setSingleCanvasDialogOpen((p) => !p),
72
+ [],
73
+ );
35
74
 
36
75
  const props = {
76
+ containerRef: bridgedScrollRef,
37
77
  ...targetProps,
38
78
  listContainerComponent: CanvasListItem,
39
79
  };
40
80
 
81
+ const contextValue = useMemo(() => ({
82
+ addCompanionWindow,
83
+ annotationEditCompanionWindowIsOpened,
84
+ annotationsOnCanvases,
85
+ canvases,
86
+ config,
87
+ receiveAnnotation,
88
+ storageAdapter: config.annotation.adapter,
89
+ t,
90
+ toggleSingleCanvasDialogOpen,
91
+ windowId: targetProps.windowId,
92
+ windowViewType,
93
+ }), [
94
+ addCompanionWindow,
95
+ annotationEditCompanionWindowIsOpened,
96
+ annotationsOnCanvases,
97
+ canvases,
98
+ config,
99
+ receiveAnnotation,
100
+ t,
101
+ toggleSingleCanvasDialogOpen,
102
+ targetProps.windowId,
103
+ windowViewType,
104
+ ]);
41
105
  return (
42
- <AnnotationActionsContext.Provider
43
- value={{
44
- addCompanionWindow,
45
- annotationEditCompanionWindowIsOpened,
46
- annotationsOnCanvases,
47
- canvases,
48
- config,
49
- receiveAnnotation,
50
- storageAdapter: config.annotation.adapter,
51
- toggleSingleCanvasDialogOpen,
52
- windowId: targetProps.windowId,
53
- windowViewType,
54
- t,
55
- }}
56
- >
57
- <TargetComponent {...props} ref={containerRef} />
106
+ <AnnotationActionsContext.Provider value={contextValue}>
107
+ <div ref={wrapperRef} style={{ height: '100%', position: 'relative' }}>
108
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */}
109
+ <TargetComponent {...props} />
110
+ </div>
111
+
58
112
  {windowViewType !== 'single' && (
59
113
  <SingleCanvasDialog
60
114
  handleClose={toggleSingleCanvasDialogOpen}
@@ -69,107 +123,77 @@ function CanvasAnnotationsWrapper({
69
123
  CanvasAnnotationsWrapper.propTypes = {
70
124
  addCompanionWindow: PropTypes.func.isRequired,
71
125
  annotationEditCompanionWindowIsOpened: PropTypes.bool.isRequired,
72
- annotationsOnCanvases: PropTypes.shape({
73
- id: PropTypes.string,
74
- isFetching: PropTypes.bool,
75
- json: PropTypes.shape({
76
- id: PropTypes.string,
77
- items: PropTypes.arrayOf(
78
- PropTypes.shape({
79
- body: PropTypes.shape({
80
- format: PropTypes.string,
81
- id: PropTypes.string,
82
- value: PropTypes.string,
83
- }),
84
- drawingState: PropTypes.string,
85
- id: PropTypes.string,
86
- manifestNetwork: PropTypes.string,
87
- motivation: PropTypes.string,
88
- target: PropTypes.string,
89
- type: PropTypes.string,
90
- }),
91
- ),
92
- type: PropTypes.string,
93
- }),
94
- }).isRequired,
95
- canvases: PropTypes.arrayOf(
96
- PropTypes.shape({
97
- id: PropTypes.string,
98
- index: PropTypes.number,
99
- }),
100
- ).isRequired,
101
- config: PropTypes.shape({
102
- annotation: PropTypes.shape({
103
- adapter: PropTypes.func,
104
- }),
105
- }).isRequired,
106
- containerRef: PropTypes.oneOfType([
107
- PropTypes.func,
108
- PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
109
- ]),
126
+ annotationsOnCanvases: PropTypes.shape({}).isRequired,
127
+ // eslint-disable-next-line max-len
128
+ canvases: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string, index: PropTypes.number })).isRequired,
129
+ config: PropTypes.shape({ annotation: PropTypes.shape({ adapter: PropTypes.func }) }).isRequired,
110
130
  receiveAnnotation: PropTypes.func.isRequired,
111
131
  switchToSingleCanvasView: PropTypes.func.isRequired,
112
132
  t: PropTypes.func.isRequired,
113
- TargetComponent: PropTypes.oneOfType([
114
- PropTypes.func,
115
- PropTypes.node,
116
- ]).isRequired,
133
+ TargetComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired,
117
134
  // eslint-disable-next-line react/forbid-prop-types
118
135
  targetProps: PropTypes.object.isRequired,
119
136
  windowViewType: PropTypes.string.isRequired,
120
137
  };
121
138
 
122
- /** TODO this logic is duplicated */
139
+ /** mapStateToProps / mapDispatchToProps unchanged from your version */
123
140
  function mapStateToProps(state, { targetProps: { windowId } }) {
124
141
  const canvases = getVisibleCanvases(state, { windowId });
125
142
  const annotationsOnCanvases = {};
126
- const annotationCreationCompanionWindows = getCompanionWindowsForContent(state, {
127
- content: 'annotationCreation',
128
- windowId,
129
- });
130
- let annotationEditCompanionWindowIsOpened = true;
131
-
132
- if (Object.keys(annotationCreationCompanionWindows).length !== 0) {
133
- annotationEditCompanionWindowIsOpened = false;
134
- }
143
+ const creation = getCompanionWindowsForContent(state, { content: 'annotationCreation', windowId });
144
+ const annotationEditCompanionWindowIsOpened = Object.keys(creation).length === 0;
135
145
 
136
146
  canvases.forEach((canvas) => {
137
147
  const anno = state.annotations[canvas.id];
138
- if (anno) {
139
- annotationsOnCanvases[canvas.id] = anno;
140
- }
148
+ if (anno) annotationsOnCanvases[canvas.id] = anno;
141
149
  });
150
+
142
151
  return {
143
152
  annotationEditCompanionWindowIsOpened,
144
153
  annotationsOnCanvases,
145
154
  canvases,
146
- config: {
147
- ...state.config,
148
- translations,
149
- },
155
+ config: { ...state.config, translations },
150
156
  windowViewType: getWindowViewType(state, { windowId }),
151
157
  };
152
158
  }
153
-
154
- /** */
155
- const mapDispatchToProps = (dispatch, props, annotationEditCompanionWindowIsOpened) => ({
156
- addCompanionWindow: (content, additionalProps) => dispatch(
157
- actions.addCompanionWindow(props.targetProps.windowId, { content, ...additionalProps }),
158
- ),
159
+ /**
160
+ * Map Redux dispatch actions to props for the CanvasAnnotationsWrapper.
161
+ *
162
+ * Provides callback props that allow the wrapped component to interact
163
+ * with the Mirador Redux store, including:
164
+ *
165
+ * - `addCompanionWindow`: Open a companion window for the given window ID,
166
+ * with specified content and optional extra props.
167
+ * - `receiveAnnotation`: Add or update an annotation in the Redux store
168
+ * for a specific target.
169
+ * - `switchToSingleCanvasView`: Change the current window's view type
170
+ * to `"single"`.
171
+ *
172
+ * @function
173
+ * @param {Function} dispatch - Redux dispatch function.
174
+ * @param {object} props - The wrapper component props.
175
+ * @param {object} props.targetProps - Props passed down to the wrapped target component.
176
+ * @param {string} props.targetProps.windowId - The ID of the Mirador window.
177
+ * @returns {object} An object mapping action dispatchers to props.
178
+ * @property {function(string, object):void} addCompanionWindow
179
+ * @property {function(string, string, object):void} receiveAnnotation
180
+ * @property {function():void} switchToSingleCanvasView
181
+ */
182
+ const mapDispatchToProps = (dispatch, props) => ({
183
+ addCompanionWindow: (content, additionalProps) => dispatch(actions.addCompanionWindow(
184
+ props.targetProps.windowId,
185
+ { content, ...additionalProps },
186
+ )),
159
187
  receiveAnnotation: (targetId, id, annotation) => dispatch(
160
188
  actions.receiveAnnotation(targetId, id, annotation),
161
189
  ),
162
- switchToSingleCanvasView: () => dispatch(
163
- actions.setWindowViewType(props.targetProps.windowId, 'single'),
164
- ),
190
+ switchToSingleCanvasView: () => dispatch(actions.setWindowViewType(props.targetProps.windowId, 'single')),
165
191
  });
166
-
167
- const CanvasAnnotationsWrapperContainer = {
192
+ const canvasAnnotationsPlugin = {
168
193
  component: CanvasAnnotationsWrapper,
169
194
  mapDispatchToProps,
170
195
  mapStateToProps,
171
196
  mode: 'wrap',
172
197
  target: 'CanvasAnnotations',
173
198
  };
174
-
175
- export default CanvasAnnotationsWrapperContainer;
199
+ export default canvasAnnotationsPlugin;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Utilities
3
+ */
4
+ export const isScrollable = (el) => {
5
+ if (!el) return false;
6
+ const cs = getComputedStyle(el);
7
+ const oy = cs.overflowY || cs.overflow || '';
8
+ return /(auto|scroll|overlay)/.test(oy) && el.scrollHeight > el.clientHeight;
9
+ };
10
+
11
+ /**
12
+ * Find the closest scrollable ancestor of a given DOM node.
13
+ *
14
+ * - Walks up the DOM tree from the node's parent using `parentElement`.
15
+ * - Returns the first ancestor element that is considered scrollable,
16
+ * meaning:
17
+ * - Its `overflowY` (or `overflow`) style is `auto`, `scroll`, or `overlay`,
18
+ * - And its `scrollHeight` is greater than its `clientHeight`.
19
+ * - Returns `null` if no scrollable ancestor is found before reaching the root.
20
+ *
21
+ * @param {HTMLElement|null} node - The starting DOM node.
22
+ * @returns {HTMLElement|null} The nearest scrollable ancestor, or null if none exist.
23
+ */
24
+ export const closestScrollableAncestor = (node) => {
25
+ let cur = node?.parentElement || null;
26
+ while (cur) {
27
+ if (isScrollable(cur)) return cur;
28
+ cur = cur.parentElement;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ /**
34
+ * Get the primary scrollable element for the browser window.
35
+ *
36
+ * - Modern browsers expose `document.scrollingElement` (usually the `<html>` element).
37
+ * - As a fallback, returns `document.documentElement` (the `<html>` element).
38
+ * - This element's `scrollTop` and `scrollHeight` represent the window's scroll state.
39
+ *
40
+ * @function
41
+ * @returns {HTMLElement} The DOM element representing the window's scrolling container.
42
+ */
43
+ export const getWindowScroller = () => document.scrollingElement || document.documentElement;
44
+
45
+ /**
46
+ * Compute the vertical scroll position needed to bring a node into view
47
+ * within a given scrollable container element.
48
+ *
49
+ * - Uses bounding boxes of the node and container to calculate the node's
50
+ * position relative to the container's scroll area.
51
+ * - Considers the container's `scrollTop`, `clientHeight`, and a custom
52
+ * `offsetTop` (for sticky headers/toolbars inside the container).
53
+ * - If the node is already fully visible inside the container, returns `null`.
54
+ * - If the node is above the visible area, returns the scrollTop needed
55
+ * to align its top just below the offset.
56
+ * - If the node is below the visible area, returns the scrollTop needed
57
+ * to bring its bottom into view (but not less than aligning its top).
58
+ *
59
+ * @param {HTMLElement} container - The scrollable container element.
60
+ * @param {HTMLElement} node - The DOM element to bring into view.
61
+ * @param {number} offsetTop - Extra vertical offset in pixels to keep free at the top.
62
+ * @returns {number|null} The target scrollTop value for the container, or null if already visible.
63
+ */
64
+ export function computeTargetContainer(container, node, offsetTop) {
65
+ const cRect = container.getBoundingClientRect();
66
+ const nRect = node.getBoundingClientRect();
67
+ const nodeTopInContainer = (nRect.top - cRect.top) + container.scrollTop;
68
+ const nodeBottomInContainer = (nRect.bottom - cRect.top) + container.scrollTop;
69
+ const visibleTop = container.scrollTop + offsetTop;
70
+ const visibleBottom = container.scrollTop + container.clientHeight;
71
+ const above = nodeTopInContainer < visibleTop;
72
+ const below = nodeBottomInContainer > visibleBottom;
73
+ if (!above && !below) return null;
74
+ return above ? nodeTopInContainer - offsetTop
75
+ : Math.max(nodeBottomInContainer - container.clientHeight, nodeTopInContainer - offsetTop);
76
+ }
77
+
78
+ /**
79
+ * Compute the vertical scroll position needed to bring a node into view
80
+ * within the browser window.
81
+ *
82
+ * - Uses the node's bounding box relative to the viewport.
83
+ * - Considers the current `window.scrollY` and the given `offsetTop`
84
+ * (to account for sticky headers or toolbars).
85
+ * - If the node is already fully visible, returns `null`.
86
+ * - If the node is above the visible viewport, returns the scrollY needed
87
+ * to align its top just below the offset.
88
+ * - If the node is below the viewport, returns the scrollY needed to align
89
+ * its bottom into view (but not less than aligning its top).
90
+ *
91
+ * @param {HTMLElement} node - The DOM element to bring into view.
92
+ * @param {number} offsetTop - Extra vertical offset in pixels to keep free at the top.
93
+ * @returns {number|null} The target scrollY for the window, or null if already visible.
94
+ */
95
+ export function computeTargetWindow(node, offsetTop) {
96
+ const rect = node.getBoundingClientRect();
97
+ const nodeTop = rect.top + window.scrollY;
98
+ const nodeBottom = rect.bottom + window.scrollY;
99
+ const viewTop = window.scrollY + offsetTop;
100
+ const viewBottom = window.scrollY + window.innerHeight;
101
+ const above = nodeTop < viewTop;
102
+ const below = nodeBottom > viewBottom;
103
+ if (!above && !below) return null;
104
+ return above ? nodeTop - offsetTop
105
+ : Math.max(nodeBottom - window.innerHeight, nodeTop - offsetTop);
106
+ }
107
+
108
+ /**
109
+ * Attempt a single scroll operation to bring the selected annotation node into view.
110
+ *
111
+ * - Uses double `requestAnimationFrame` to wait until layout and paints have settled.
112
+ * - Resolves the correct scroll container (bridged ref, closest ancestor, or window).
113
+ * - Computes the target scroll position using `computeTargetWindow` or
114
+ * `computeTargetContainer`.
115
+ * - Performs the scroll with the configured `scrollBehavior`.
116
+ * - After a short delay (`scrollRetryDelay`),
117
+ * checks whether the scroll position actually changed.
118
+ *
119
+ * @param {HTMLElement} node - The DOM element to scroll into view.
120
+ * @param {React.RefObject<HTMLElement>} bridgedScrollRef - Ref to a preferred scrollable container.
121
+ * @param {number} scrollRetryDelay - Delay in ms before checking if the scroll succeeded.
122
+ * @returns {Promise<boolean>} Resolves to true if the scroll moved the container, false otherwise.
123
+ */
124
+ export const runScrollOnce = (
125
+ node,
126
+ bridgedScrollRef,
127
+ scrollRetryDelay,
128
+ ) => new Promise((resolve) => {
129
+ const scrollBehavior = 'smooth';
130
+ const scrollOffsetTop = 96;
131
+
132
+ requestAnimationFrame(() => {
133
+ // eslint-disable-next-line consistent-return
134
+ requestAnimationFrame(() => {
135
+ let container = bridgedScrollRef.current;
136
+ if (!isScrollable(container)) container = closestScrollableAncestor(node);
137
+ if (!isScrollable(container)) container = getWindowScroller();
138
+
139
+ const isWindow = container === document.body
140
+ || container === document.documentElement
141
+ || container === document.scrollingElement;
142
+
143
+ if (isWindow) {
144
+ const topWindow = computeTargetWindow(node, scrollOffsetTop);
145
+ if (topWindow == null) return resolve(true);
146
+ const before = window.scrollY;
147
+ window.scrollTo({
148
+ behavior: scrollBehavior,
149
+ top: topWindow,
150
+ });
151
+ setTimeout(() => {
152
+ const after = window.scrollY;
153
+ resolve(Math.abs(after - before) > 0.5);
154
+ }, scrollRetryDelay);
155
+ // eslint-disable-next-line consistent-return
156
+ return;
157
+ }
158
+
159
+ const top = computeTargetContainer(container, node, scrollOffsetTop);
160
+ if (top == null) return resolve(true);
161
+ const before = container.scrollTop;
162
+ if (typeof container.scrollTo === 'function') {
163
+ container.scrollTo({
164
+ behavior: scrollBehavior,
165
+ top,
166
+ });
167
+ }
168
+ setTimeout(() => {
169
+ const after = container.scrollTop;
170
+ resolve(Math.abs(after - before) > 0.5);
171
+ }, scrollRetryDelay);
172
+ });
173
+ });
174
+ });
175
+
176
+ /**
177
+ * Scroll the selected annotation into view, retrying if necessary.
178
+ * @param node
179
+ * @param bridgedScrollRef
180
+ * @returns {Promise<void>}
181
+ */
182
+ export const scrollToSelectedAnnotation = async (node, bridgedScrollRef) => {
183
+ const maxScrollRetries = 3;
184
+ const scrollRetryDelay = 24;
185
+
186
+ // eslint-disable-next-line no-plusplus
187
+ for (let attempt = 0; attempt <= maxScrollRetries; attempt++) {
188
+ // eslint-disable-next-line no-await-in-loop
189
+ const ok = await runScrollOnce(node, bridgedScrollRef, scrollRetryDelay);
190
+ if (ok) {
191
+ break;
192
+ }
193
+
194
+ if (attempt < maxScrollRetries) {
195
+ // eslint-disable-next-line no-await-in-loop
196
+ await new Promise((resolve) => { setTimeout(resolve, scrollRetryDelay); });
197
+ }
198
+ }
199
+ };
@@ -1,58 +1,12 @@
1
- import React, { useEffect, useCallback } from 'react';
1
+ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
4
- import * as actions from 'mirador/dist/es/src/state/actions';
5
- import LocalStorageAdapter from '../annotationAdapter/LocalStorageAdapter';
6
- import AnnototAdapter from '../annotationAdapter/AnnototAdapter';
7
- import { AnnotationAdapter } from '../annotationAdapter/AnnotationAdapterUtils';
8
3
 
9
4
  /** Functional component version of ExternalStorageAnnotation */
10
5
  function ExternalStorageAnnotation({
11
- canvases,
12
- config,
13
- receiveAnnotation,
14
- PluginComponents,
6
+ PluginComponents = [],
15
7
  TargetComponent,
16
8
  targetProps,
17
9
  }) {
18
- const retrieveAnnotations = useCallback((currentCanvases) => {
19
- currentCanvases.forEach((canvas) => {
20
- if (
21
- typeof config.annotation.adapter === 'string'
22
- && config.annotation.adapter === AnnotationAdapter.LOCAL_STORAGE
23
- ) {
24
- config.annotation.adapter = (canvasId) =>
25
- new LocalStorageAdapter(`localStorage://?canvasId=${canvasId}`);
26
- }
27
- if (
28
- typeof config.annotation.adapter === 'string'
29
- && config.annotation.adapter === AnnotationAdapter.ANNOTOT
30
- ) {
31
- const endpointUrl = 'http://127.0.0.1:3000/annotations';
32
- config.annotation.adapter = (canvasId) => new AnnototAdapter(canvasId, endpointUrl);
33
- }
34
- if (!config.annotation.adapter) {
35
- config.annotation.adapter = (canvasId) =>
36
- new LocalStorageAdapter(`localStorage://?canvasId=${canvasId}`);
37
- }
38
- const storageAdapter = config.annotation.adapter(canvas.id);
39
-
40
- storageAdapter.all().then((annoPage) => {
41
- if (annoPage) {
42
- receiveAnnotation(
43
- canvas.id,
44
- storageAdapter.annotationPageId,
45
- annoPage,
46
- );
47
- }
48
- });
49
- });
50
- }, [config, receiveAnnotation]);
51
-
52
- useEffect(() => {
53
- retrieveAnnotations(canvases);
54
- }, [canvases, retrieveAnnotations]);
55
-
56
10
  return (
57
11
  <TargetComponent
58
12
  {...targetProps} // eslint-disable-line react/jsx-props-no-spreading
@@ -62,16 +16,7 @@ function ExternalStorageAnnotation({
62
16
  }
63
17
 
64
18
  ExternalStorageAnnotation.propTypes = {
65
- canvases: PropTypes.arrayOf(
66
- PropTypes.shape({ id: PropTypes.string, index: PropTypes.number }),
67
- ),
68
- config: PropTypes.shape({
69
- annotation: PropTypes.shape({
70
- adapter: PropTypes.func,
71
- }),
72
- }).isRequired,
73
- PluginComponents: PropTypes.array, // eslint-disable-line react/forbid-prop-types
74
- receiveAnnotation: PropTypes.func.isRequired,
19
+ PluginComponents: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
75
20
  TargetComponent: PropTypes.oneOfType([
76
21
  PropTypes.func,
77
22
  PropTypes.node,
@@ -79,28 +24,10 @@ ExternalStorageAnnotation.propTypes = {
79
24
  targetProps: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
80
25
  };
81
26
 
82
- ExternalStorageAnnotation.defaultProps = {
83
- canvases: [],
84
- PluginComponents: [],
85
- };
86
-
87
- /** */
88
- const mapDispatchToProps = {
89
- receiveAnnotation: actions.receiveAnnotation,
90
- };
91
-
92
- /** */
93
- function mapStateToProps(state, { targetProps }) {
94
- return {
95
- canvases: getVisibleCanvases(state, { windowId: targetProps.windowId }),
96
- config: state.config,
97
- };
98
- }
99
-
100
- export default {
27
+ const externalStorageAnnotationPlugin = {
101
28
  component: ExternalStorageAnnotation,
102
- mapDispatchToProps,
103
- mapStateToProps,
104
29
  mode: 'wrap',
105
30
  target: 'Window',
106
31
  };
32
+
33
+ export default externalStorageAnnotationPlugin;
@@ -8,9 +8,11 @@ import { MiradorMenuButton } from 'mirador/dist/es/src/components/MiradorMenuBut
8
8
  import { getVisibleCanvases } from 'mirador/dist/es/src/state/selectors/canvases';
9
9
  import { useDispatch, useSelector } from 'react-redux';
10
10
  import { Tooltip } from '@mui/material';
11
+ import { getCompanionWindowsForContent } from 'mirador/dist/es/src/state/selectors/companionWindows';
11
12
  import SingleCanvasDialog from '../SingleCanvasDialog';
12
13
  import AnnotationExportDialog from '../AnnotationExportDialog';
13
14
  import LocalStorageAdapter from '../annotationAdapter/LocalStorageAdapter';
15
+ import translations from '../locales/locales';
14
16
 
15
17
  /** Mirador annotation plugin component. Get all the stuff
16
18
  * and info to manage annotation functionnality */
@@ -86,7 +88,6 @@ function MiradorAnnotation(
86
88
  open={singleCanvasDialogOpen}
87
89
  handleClose={toggleSingleCanvasDialogOpen}
88
90
  switchToSingleCanvasView={switchToSingleCanvasView}
89
- t={t}
90
91
  />
91
92
  )}
92
93
  {offerExportDialog && (
@@ -106,7 +107,6 @@ function MiradorAnnotation(
106
107
  config={config}
107
108
  handleClose={toggleCanvasExportDialog}
108
109
  open={annotationExportDialogOpen}
109
- t={t}
110
110
  />
111
111
  )}
112
112
  </div>
@@ -124,7 +124,6 @@ MiradorAnnotation.propTypes = {
124
124
  exportLocalStorageAnnotations: PropTypes.bool,
125
125
  }),
126
126
  }).isRequired,
127
- createAnnotation: PropTypes.bool.isRequired,
128
127
  t: PropTypes.func.isRequired,
129
128
  TargetComponent: PropTypes.oneOfType([
130
129
  PropTypes.func,
@@ -135,4 +134,33 @@ MiradorAnnotation.propTypes = {
135
134
  windowViewType: PropTypes.string.isRequired,
136
135
  };
137
136
 
138
- export default MiradorAnnotation;
137
+ // TODO use selector in main componenent
138
+ /**
139
+ * this function map the state to the annotationPlugin's props
140
+ * */
141
+ function mapStateToProps(state, { targetProps: { windowId } }) {
142
+ // Annotation edit companion window ou annotation creation companion window is the same thing
143
+ const annotationCreationCompanionWindows = getCompanionWindowsForContent(state, { content: 'annotationCreation', windowId });
144
+ let annotationEditCompanionWindowIsOpened = true;
145
+ if (Object.keys(annotationCreationCompanionWindows).length !== 0) {
146
+ annotationEditCompanionWindowIsOpened = false;
147
+ }
148
+ return {
149
+ annotationEditCompanionWindowIsOpened,
150
+ canvases: getVisibleCanvases(state, { windowId }),
151
+ config: state.config,
152
+ windowViewType: getWindowViewType(state, { windowId }),
153
+ };
154
+ }
155
+
156
+ const miradorAnnotationPlugin = {
157
+ component: MiradorAnnotation,
158
+ config: {
159
+ translations,
160
+ },
161
+ mapStateToProps,
162
+ mode: 'wrap',
163
+ target: 'AnnotationSettings',
164
+ };
165
+
166
+ export default miradorAnnotationPlugin;