react-cheminfo 0.16.0 → 0.18.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.
Files changed (170) hide show
  1. package/lib/parallel/core/index.d.ts +7 -5
  2. package/lib/parallel/core/index.d.ts.map +1 -1
  3. package/lib/parallel/core/index.js +5 -3
  4. package/lib/parallel/core/index.js.map +1 -1
  5. package/lib/parallel/core/parallelBrush.d.ts +22 -1
  6. package/lib/parallel/core/parallelBrush.d.ts.map +1 -1
  7. package/lib/parallel/core/parallelBrush.js +53 -3
  8. package/lib/parallel/core/parallelBrush.js.map +1 -1
  9. package/lib/parallel/core/parallelFilter.d.ts +14 -1
  10. package/lib/parallel/core/parallelFilter.d.ts.map +1 -1
  11. package/lib/parallel/core/parallelFilter.js +44 -11
  12. package/lib/parallel/core/parallelFilter.js.map +1 -1
  13. package/lib/parallel/core/parallelOrder.d.ts +36 -0
  14. package/lib/parallel/core/parallelOrder.d.ts.map +1 -0
  15. package/lib/parallel/core/parallelOrder.js +65 -0
  16. package/lib/parallel/core/parallelOrder.js.map +1 -0
  17. package/lib/parallel/core/parallelPaint.d.ts +4 -0
  18. package/lib/parallel/core/parallelPaint.d.ts.map +1 -1
  19. package/lib/parallel/core/parallelPaint.js +15 -6
  20. package/lib/parallel/core/parallelPaint.js.map +1 -1
  21. package/lib/parallel/core/parallelPalette.d.ts +10 -1
  22. package/lib/parallel/core/parallelPalette.d.ts.map +1 -1
  23. package/lib/parallel/core/parallelPalette.js +14 -1
  24. package/lib/parallel/core/parallelPalette.js.map +1 -1
  25. package/lib/parallel/core/parallelSelection.d.ts +43 -0
  26. package/lib/parallel/core/parallelSelection.d.ts.map +1 -0
  27. package/lib/parallel/core/parallelSelection.js +87 -0
  28. package/lib/parallel/core/parallelSelection.js.map +1 -0
  29. package/lib/parallel/core/parallelTicks.d.ts.map +1 -1
  30. package/lib/parallel/core/parallelTicks.js +7 -4
  31. package/lib/parallel/core/parallelTicks.js.map +1 -1
  32. package/lib/parallel/core/parallelTypes.d.ts +12 -1
  33. package/lib/parallel/core/parallelTypes.d.ts.map +1 -1
  34. package/lib/parallel/core/parallelTypes.js.map +1 -1
  35. package/lib/parallel/ui/ParallelAxesLayer.d.ts +2 -2
  36. package/lib/parallel/ui/ParallelAxesLayer.d.ts.map +1 -1
  37. package/lib/parallel/ui/ParallelAxesLayer.js +2 -2
  38. package/lib/parallel/ui/ParallelAxesLayer.js.map +1 -1
  39. package/lib/parallel/ui/ParallelCoordinates.d.ts.map +1 -1
  40. package/lib/parallel/ui/ParallelCoordinates.js +10 -5
  41. package/lib/parallel/ui/ParallelCoordinates.js.map +1 -1
  42. package/lib/parallel/ui/ParallelLabels.d.ts +6 -0
  43. package/lib/parallel/ui/ParallelLabels.d.ts.map +1 -1
  44. package/lib/parallel/ui/ParallelLabels.js +14 -2
  45. package/lib/parallel/ui/ParallelLabels.js.map +1 -1
  46. package/lib/parallel/ui/index.d.ts +4 -0
  47. package/lib/parallel/ui/index.d.ts.map +1 -1
  48. package/lib/parallel/ui/index.js +2 -0
  49. package/lib/parallel/ui/index.js.map +1 -1
  50. package/lib/parallel/ui/parallelCoordinatesProps.d.ts +26 -8
  51. package/lib/parallel/ui/parallelCoordinatesProps.d.ts.map +1 -1
  52. package/lib/parallel/ui/parallelGestureModel.d.ts +37 -6
  53. package/lib/parallel/ui/parallelGestureModel.d.ts.map +1 -1
  54. package/lib/parallel/ui/parallelGestureModel.js +42 -12
  55. package/lib/parallel/ui/parallelGestureModel.js.map +1 -1
  56. package/lib/parallel/ui/parallelPlotModel.d.ts +1 -1
  57. package/lib/parallel/ui/parallelPlotModel.d.ts.map +1 -1
  58. package/lib/parallel/ui/parallelPlotModel.js +3 -3
  59. package/lib/parallel/ui/parallelPlotModel.js.map +1 -1
  60. package/lib/parallel/ui/parallelStyles.d.ts +26 -3
  61. package/lib/parallel/ui/parallelStyles.d.ts.map +1 -1
  62. package/lib/parallel/ui/parallelStyles.js +26 -4
  63. package/lib/parallel/ui/parallelStyles.js.map +1 -1
  64. package/lib/parallel/ui/useParallelAxisDrag.d.ts +60 -0
  65. package/lib/parallel/ui/useParallelAxisDrag.d.ts.map +1 -0
  66. package/lib/parallel/ui/useParallelAxisDrag.js +96 -0
  67. package/lib/parallel/ui/useParallelAxisDrag.js.map +1 -0
  68. package/lib/parallel/ui/useParallelGesture.d.ts +10 -5
  69. package/lib/parallel/ui/useParallelGesture.d.ts.map +1 -1
  70. package/lib/parallel/ui/useParallelGesture.js +46 -21
  71. package/lib/parallel/ui/useParallelGesture.js.map +1 -1
  72. package/lib/structure/core/editorGuide.d.ts +69 -0
  73. package/lib/structure/core/editorGuide.d.ts.map +1 -0
  74. package/lib/structure/core/editorGuide.js +162 -0
  75. package/lib/structure/core/editorGuide.js.map +1 -0
  76. package/lib/structure/core/editorToolbar.d.ts +45 -0
  77. package/lib/structure/core/editorToolbar.d.ts.map +1 -0
  78. package/lib/structure/core/editorToolbar.js +132 -0
  79. package/lib/structure/core/editorToolbar.js.map +1 -0
  80. package/lib/structure/core/editorToolbarGeometry.d.ts +36 -0
  81. package/lib/structure/core/editorToolbarGeometry.d.ts.map +1 -0
  82. package/lib/structure/core/editorToolbarGeometry.js +61 -0
  83. package/lib/structure/core/editorToolbarGeometry.js.map +1 -0
  84. package/lib/structure/core/index.d.ts +6 -0
  85. package/lib/structure/core/index.d.ts.map +1 -1
  86. package/lib/structure/core/index.js +3 -0
  87. package/lib/structure/core/index.js.map +1 -1
  88. package/lib/structure/core/pendingCall.d.ts +21 -0
  89. package/lib/structure/core/pendingCall.d.ts.map +1 -0
  90. package/lib/structure/core/pendingCall.js +37 -0
  91. package/lib/structure/core/pendingCall.js.map +1 -0
  92. package/lib/structure/ui/EditorCanvas.d.ts +1 -16
  93. package/lib/structure/ui/EditorCanvas.d.ts.map +1 -1
  94. package/lib/structure/ui/EditorCanvas.js +5 -18
  95. package/lib/structure/ui/EditorCanvas.js.map +1 -1
  96. package/lib/structure/ui/EditorHelpButton.d.ts +22 -0
  97. package/lib/structure/ui/EditorHelpButton.d.ts.map +1 -0
  98. package/lib/structure/ui/EditorHelpButton.js +38 -0
  99. package/lib/structure/ui/EditorHelpButton.js.map +1 -0
  100. package/lib/structure/ui/KeyCaps.d.ts +12 -0
  101. package/lib/structure/ui/KeyCaps.d.ts.map +1 -0
  102. package/lib/structure/ui/KeyCaps.js +29 -0
  103. package/lib/structure/ui/KeyCaps.js.map +1 -0
  104. package/lib/structure/ui/StructureEditor.d.ts +9 -2
  105. package/lib/structure/ui/StructureEditor.d.ts.map +1 -1
  106. package/lib/structure/ui/StructureEditor.js +17 -24
  107. package/lib/structure/ui/StructureEditor.js.map +1 -1
  108. package/lib/structure/ui/StructureEditorHelp.d.ts +18 -0
  109. package/lib/structure/ui/StructureEditorHelp.d.ts.map +1 -0
  110. package/lib/structure/ui/StructureEditorHelp.js +68 -0
  111. package/lib/structure/ui/StructureEditorHelp.js.map +1 -0
  112. package/lib/structure/ui/ToolbarTooltip.d.ts +20 -0
  113. package/lib/structure/ui/ToolbarTooltip.d.ts.map +1 -0
  114. package/lib/structure/ui/ToolbarTooltip.js +49 -0
  115. package/lib/structure/ui/ToolbarTooltip.js.map +1 -0
  116. package/lib/structure/ui/editorChange.d.ts +52 -0
  117. package/lib/structure/ui/editorChange.d.ts.map +1 -0
  118. package/lib/structure/ui/editorChange.js +34 -0
  119. package/lib/structure/ui/editorChange.js.map +1 -0
  120. package/lib/structure/ui/index.d.ts +3 -1
  121. package/lib/structure/ui/index.d.ts.map +1 -1
  122. package/lib/structure/ui/index.js +1 -0
  123. package/lib/structure/ui/index.js.map +1 -1
  124. package/lib/structure/ui/useToolbarFloor.d.ts.map +1 -1
  125. package/lib/structure/ui/useToolbarFloor.js +6 -45
  126. package/lib/structure/ui/useToolbarFloor.js.map +1 -1
  127. package/lib/structure/ui/useToolbarHover.d.ts +28 -0
  128. package/lib/structure/ui/useToolbarHover.d.ts.map +1 -0
  129. package/lib/structure/ui/useToolbarHover.js +96 -0
  130. package/lib/structure/ui/useToolbarHover.js.map +1 -0
  131. package/lib/structure/ui/watchToolbar.d.ts +17 -0
  132. package/lib/structure/ui/watchToolbar.d.ts.map +1 -0
  133. package/lib/structure/ui/watchToolbar.js +56 -0
  134. package/lib/structure/ui/watchToolbar.js.map +1 -0
  135. package/package.json +1 -1
  136. package/src/parallel/core/index.ts +16 -0
  137. package/src/parallel/core/parallelBrush.ts +69 -3
  138. package/src/parallel/core/parallelFilter.ts +52 -15
  139. package/src/parallel/core/parallelOrder.ts +73 -0
  140. package/src/parallel/core/parallelPaint.ts +16 -5
  141. package/src/parallel/core/parallelPalette.ts +14 -1
  142. package/src/parallel/core/parallelSelection.ts +99 -0
  143. package/src/parallel/core/parallelTicks.ts +7 -3
  144. package/src/parallel/core/parallelTypes.ts +13 -1
  145. package/src/parallel/ui/ParallelAxesLayer.tsx +10 -4
  146. package/src/parallel/ui/ParallelCoordinates.tsx +22 -3
  147. package/src/parallel/ui/ParallelLabels.tsx +23 -2
  148. package/src/parallel/ui/index.ts +15 -0
  149. package/src/parallel/ui/parallelCoordinatesProps.ts +26 -8
  150. package/src/parallel/ui/parallelGestureModel.ts +63 -13
  151. package/src/parallel/ui/parallelPlotModel.ts +3 -3
  152. package/src/parallel/ui/parallelStyles.ts +46 -4
  153. package/src/parallel/ui/useParallelAxisDrag.ts +147 -0
  154. package/src/parallel/ui/useParallelGesture.ts +61 -28
  155. package/src/structure/core/editorGuide.ts +221 -0
  156. package/src/structure/core/editorToolbar.ts +175 -0
  157. package/src/structure/core/editorToolbarGeometry.ts +80 -0
  158. package/src/structure/core/index.ts +29 -0
  159. package/src/structure/core/pendingCall.ts +59 -0
  160. package/src/structure/ui/EditorCanvas.tsx +9 -35
  161. package/src/structure/ui/EditorHelpButton.tsx +70 -0
  162. package/src/structure/ui/KeyCaps.tsx +40 -0
  163. package/src/structure/ui/StructureEditor.tsx +39 -21
  164. package/src/structure/ui/StructureEditorHelp.tsx +136 -0
  165. package/src/structure/ui/ToolbarTooltip.tsx +95 -0
  166. package/src/structure/ui/editorChange.ts +81 -0
  167. package/src/structure/ui/index.ts +5 -1
  168. package/src/structure/ui/useToolbarFloor.ts +7 -46
  169. package/src/structure/ui/useToolbarHover.ts +134 -0
  170. package/src/structure/ui/watchToolbar.ts +59 -0
@@ -0,0 +1,70 @@
1
+ import { Button, PopoverNext, Tooltip } from '@blueprintjs/core';
2
+ import type { CSSProperties, ReactElement, RefObject } from 'react';
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import { StructureEditorHelp } from './StructureEditorHelp.tsx';
6
+ import type { StructureEditorMode } from './editorChange.ts';
7
+
8
+ /** What {@link EditorHelpButton} needs. */
9
+ export interface EditorHelpButtonProps {
10
+ /** The element wrapping the editor, which F1 is listened for on. */
11
+ containerRef: RefObject<HTMLElement | null>;
12
+ /** Which editor the guide is written for. */
13
+ mode: StructureEditorMode;
14
+ /** Whether the editor draws a query fragment. */
15
+ fragment: boolean;
16
+ }
17
+
18
+ /**
19
+ * The editor's help button, in the corner of the drawing, opening the guide to
20
+ * the mouse and the keyboard.
21
+ *
22
+ * F1 opens it too: openchemlib binds that key to a help dialog it does not
23
+ * implement on the web.
24
+ * @param props - See {@link EditorHelpButtonProps}.
25
+ * @returns The button and its popover.
26
+ */
27
+ export function EditorHelpButton(props: EditorHelpButtonProps): ReactElement {
28
+ const { containerRef, mode, fragment } = props;
29
+ const [isOpen, setIsOpen] = useState(false);
30
+
31
+ useEffect(() => {
32
+ const container = containerRef.current;
33
+ if (container === null) return;
34
+ const onKeyDown = (event: KeyboardEvent): void => {
35
+ if (event.key !== 'F1') return;
36
+ event.preventDefault();
37
+ setIsOpen(true);
38
+ };
39
+ container.addEventListener('keydown', onKeyDown);
40
+ return () => container.removeEventListener('keydown', onKeyDown);
41
+ }, [containerRef]);
42
+
43
+ return (
44
+ <div style={CORNER_STYLE}>
45
+ <PopoverNext
46
+ isOpen={isOpen}
47
+ onInteraction={setIsOpen}
48
+ placement="left-start"
49
+ content={<StructureEditorHelp mode={mode} fragment={fragment} />}
50
+ >
51
+ <Tooltip content={LABEL} placement="left" disabled={isOpen}>
52
+ <Button
53
+ variant="minimal"
54
+ size="small"
55
+ icon="help"
56
+ aria-label={LABEL}
57
+ />
58
+ </Tooltip>
59
+ </PopoverNext>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ const LABEL = 'Mouse and keyboard';
65
+
66
+ const CORNER_STYLE: CSSProperties = {
67
+ position: 'absolute',
68
+ top: 4,
69
+ right: 4,
70
+ };
@@ -0,0 +1,40 @@
1
+ import { Classes } from '@blueprintjs/core';
2
+ import type { CSSProperties, ReactElement } from 'react';
3
+
4
+ import { editorKeyLabel } from '../core/editorGuide.ts';
5
+
6
+ /**
7
+ * Keys pressed together, each drawn as a key cap, `Mod` as the modifier the
8
+ * editor listens to on this platform.
9
+ * @param props - The keys.
10
+ * @param props.keys - The keys, unique within the list.
11
+ * @returns The key caps.
12
+ */
13
+ export function KeyCaps(props: { keys: readonly string[] }): ReactElement {
14
+ const isMac = isMacPlatform();
15
+ return (
16
+ <span style={KEYS_STYLE}>
17
+ {props.keys.map((key) => (
18
+ <kbd key={key} className={Classes.KEY}>
19
+ {editorKeyLabel(key, isMac)}
20
+ </kbd>
21
+ ))}
22
+ </span>
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Whether the page runs on a Mac, decided as openchemlib decides it, so the
28
+ * cap shown is the modifier the editor actually reads: ⌘ there, Ctrl elsewhere.
29
+ * @returns True on a Mac.
30
+ */
31
+ function isMacPlatform(): boolean {
32
+ return typeof navigator !== 'undefined' && navigator.platform === 'MacIntel';
33
+ }
34
+
35
+ const KEYS_STYLE: CSSProperties = {
36
+ display: 'inline-flex',
37
+ alignItems: 'center',
38
+ gap: 3,
39
+ whiteSpace: 'nowrap',
40
+ };
@@ -8,15 +8,25 @@
8
8
  */
9
9
 
10
10
  import type { CSSProperties, ReactElement } from 'react';
11
- import { Suspense, lazy, useCallback, useEffect, useRef } from 'react';
11
+ import {
12
+ Suspense,
13
+ lazy,
14
+ useCallback,
15
+ useEffect,
16
+ useRef,
17
+ useState,
18
+ } from 'react';
12
19
  import type { CanvasEditorInputFormat } from 'react-ocl';
13
20
 
14
21
  import { TOKEN } from '../../tokens/core/familyTokens.ts';
22
+ import { createPendingCall } from '../core/pendingCall.ts';
15
23
 
24
+ import { EditorHelpButton } from './EditorHelpButton.tsx';
25
+ import { ToolbarTooltip } from './ToolbarTooltip.tsx';
16
26
  import type {
17
27
  StructureEditorChange,
18
28
  StructureEditorMode,
19
- } from './EditorCanvas.tsx';
29
+ } from './editorChange.ts';
20
30
  import { useToolbarFloor } from './useToolbarFloor.ts';
21
31
 
22
32
  const EditorCanvas = lazy(async () => {
@@ -76,6 +86,12 @@ export interface StructureEditorProps {
76
86
  * @default 'molecule'
77
87
  */
78
88
  mode?: StructureEditorMode;
89
+ /**
90
+ * Explain each toolbar button in a tooltip, and put a button in the corner
91
+ * that opens a guide to the mouse and the keyboard, which F1 opens too.
92
+ * @default true
93
+ */
94
+ help?: boolean;
79
95
  /**
80
96
  * Class the container carries, so a site can reach it from its stylesheet.
81
97
  * @default undefined
@@ -90,7 +106,8 @@ export interface StructureEditorProps {
90
106
 
91
107
  /**
92
108
  * The canvas structure editor, sized to fill its container and never to hide
93
- * part of its toolbar.
109
+ * part of its toolbar, with a tooltip on every toolbar button and a guide to
110
+ * the keys.
94
111
  * @param props - What to draw, when to reload it, and where to send it.
95
112
  * @returns The editor.
96
113
  */
@@ -104,6 +121,7 @@ export function StructureEditor(props: StructureEditorProps): ReactElement {
104
121
  debounce = 300,
105
122
  minHeight = 320,
106
123
  mode = 'molecule',
124
+ help = true,
107
125
  className,
108
126
  style,
109
127
  } = props;
@@ -126,6 +144,16 @@ export function StructureEditor(props: StructureEditorProps): ReactElement {
126
144
  value={value}
127
145
  mode={mode}
128
146
  />
147
+ {help ? (
148
+ <>
149
+ <ToolbarTooltip containerRef={containerRef} mode={mode} />
150
+ <EditorHelpButton
151
+ containerRef={containerRef}
152
+ mode={mode}
153
+ fragment={fragment}
154
+ />
155
+ </>
156
+ ) : null}
129
157
  </Suspense>
130
158
  </div>
131
159
  );
@@ -136,6 +164,8 @@ export function StructureEditor(props: StructureEditorProps): ReactElement {
136
164
  *
137
165
  * The editor's own event is read out before the wait starts, so what arrives
138
166
  * late is a plain object rather than a handle on an editor that has moved on.
167
+ * An editor removed mid-burst — a tab switched, a card folded — still reports
168
+ * its last edit, which would otherwise be lost with the timer.
139
169
  * @param onChange - What the caller wants told.
140
170
  * @param delay - How long the drawing has to be still, in milliseconds.
141
171
  * @param resetKey - Dropped edits: a burst still waiting when the caller
@@ -147,33 +177,21 @@ function useDebounced(
147
177
  delay: number,
148
178
  resetKey: number,
149
179
  ): (change: StructureEditorChange) => void {
150
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
180
+ const [pending] = useState(createPendingCall<StructureEditorChange>);
151
181
  const onChangeRef = useRef(onChange);
152
182
  useEffect(() => {
153
183
  onChangeRef.current = onChange;
154
184
  });
155
185
  useEffect(() => {
156
- return () => {
157
- if (timerRef.current !== null) {
158
- clearTimeout(timerRef.current);
159
- timerRef.current = null;
160
- }
161
- };
162
- }, [resetKey]);
186
+ pending.drop();
187
+ }, [pending, resetKey]);
188
+ useEffect(() => () => pending.flush(), [pending]);
163
189
 
164
190
  return useCallback(
165
191
  (change: StructureEditorChange) => {
166
- if (timerRef.current !== null) clearTimeout(timerRef.current);
167
- if (delay <= 0) {
168
- onChangeRef.current(change);
169
- return;
170
- }
171
- timerRef.current = setTimeout(() => {
172
- timerRef.current = null;
173
- onChangeRef.current(change);
174
- }, delay);
192
+ pending.push(change, delay, (value) => onChangeRef.current(value));
175
193
  },
176
- [delay],
194
+ [pending, delay],
177
195
  );
178
196
  }
179
197
 
@@ -0,0 +1,136 @@
1
+ /**
2
+ * What the keyboard and the mouse do in the structure editor, shown in the
3
+ * editor's help popover, and usable by a site on a page of its own.
4
+ */
5
+
6
+ import { Classes } from '@blueprintjs/core';
7
+ import type { CSSProperties, ReactElement } from 'react';
8
+
9
+ import { TOKEN } from '../../tokens/core/familyTokens.ts';
10
+ import type { EditorGesture, EditorGuideOptions } from '../core/editorGuide.ts';
11
+ import {
12
+ STRUCTURE_EDITOR_DOCS,
13
+ editorGuideSections,
14
+ } from '../core/editorGuide.ts';
15
+
16
+ import { KeyCaps } from './KeyCaps.tsx';
17
+
18
+ /** What {@link StructureEditorHelp} is written for. */
19
+ export type StructureEditorHelpProps = EditorGuideOptions;
20
+
21
+ /**
22
+ * The guide to the structure editor: the keys that change the atom or bond
23
+ * under the pointer, the selection gestures, the stereo conventions, and links
24
+ * to the full documentation. Gestures that do nothing in the given editor are
25
+ * left out.
26
+ * @param props - See {@link StructureEditorHelpProps}.
27
+ * @returns The guide.
28
+ */
29
+ export function StructureEditorHelp(
30
+ props: StructureEditorHelpProps,
31
+ ): ReactElement {
32
+ const sections = editorGuideSections(props);
33
+
34
+ return (
35
+ <div style={PANEL_STYLE} data-testid="structure-editor-help">
36
+ <p className={Classes.TEXT_MUTED} style={PARAGRAPH_STYLE}>
37
+ Hover a toolbar button to see what it does and its key.
38
+ </p>
39
+ {/* One grid for every section, so all the actions start on one edge. */}
40
+ <div style={GRID_STYLE}>
41
+ {sections.map((section) => (
42
+ <section key={section.title} style={ROW_STYLE}>
43
+ <h4 style={HEADING_STYLE}>{section.title}</h4>
44
+ {section.gestures.map((gesture) => (
45
+ <div key={gestureKey(gesture)} style={ROW_STYLE}>
46
+ <GestureInput input={gesture.input} />
47
+ <span>{gesture.action}</span>
48
+ </div>
49
+ ))}
50
+ {section.note === undefined ? null : (
51
+ <p className={Classes.TEXT_MUTED} style={NOTE_STYLE}>
52
+ {section.note}
53
+ </p>
54
+ )}
55
+ </section>
56
+ ))}
57
+ </div>
58
+ <p style={PARAGRAPH_STYLE}>
59
+ Learn more:{' '}
60
+ {STRUCTURE_EDITOR_DOCS.map((link, index) => (
61
+ <span key={link.url}>
62
+ {index === 0 ? null : ' · '}
63
+ <a href={link.url} target="_blank" rel="noopener noreferrer">
64
+ {link.title}
65
+ </a>
66
+ </span>
67
+ ))}
68
+ </p>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function GestureInput(props: { input: EditorGesture['input'] }): ReactElement {
74
+ return (
75
+ <span style={INPUT_STYLE}>
76
+ {props.input.map((part) =>
77
+ typeof part === 'string' ? (
78
+ <span key={part}>{part}</span>
79
+ ) : (
80
+ <KeyCaps key={part.key} keys={[part.key]} />
81
+ ),
82
+ )}
83
+ </span>
84
+ );
85
+ }
86
+
87
+ function gestureKey(gesture: EditorGesture): string {
88
+ const input = gesture.input
89
+ .map((part) => (typeof part === 'string' ? part : part.key))
90
+ .join(' ');
91
+ return `${input} ${gesture.action}`;
92
+ }
93
+
94
+ const PANEL_STYLE: CSSProperties = {
95
+ display: 'flex',
96
+ flexDirection: 'column',
97
+ gap: 10,
98
+ width: 400,
99
+ maxHeight: 'min(70vh, 36rem)',
100
+ overflowY: 'auto',
101
+ padding: 12,
102
+ fontSize: 12,
103
+ };
104
+
105
+ const GRID_STYLE: CSSProperties = {
106
+ display: 'grid',
107
+ gridTemplateColumns: 'max-content minmax(0, 1fr)',
108
+ alignItems: 'center',
109
+ columnGap: 12,
110
+ rowGap: 6,
111
+ };
112
+
113
+ const ROW_STYLE: CSSProperties = { display: 'contents' };
114
+
115
+ const HEADING_STYLE: CSSProperties = {
116
+ gridColumn: '1 / -1',
117
+ margin: '8px 0 0',
118
+ paddingBottom: 3,
119
+ borderBottom: `1px solid ${TOKEN.border}`,
120
+ color: TOKEN.textMuted,
121
+ fontSize: 11,
122
+ fontWeight: 700,
123
+ letterSpacing: '0.04em',
124
+ textTransform: 'uppercase',
125
+ };
126
+
127
+ const INPUT_STYLE: CSSProperties = {
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ gap: 4,
131
+ whiteSpace: 'nowrap',
132
+ };
133
+
134
+ const PARAGRAPH_STYLE: CSSProperties = { margin: 0, lineHeight: 1.45 };
135
+
136
+ const NOTE_STYLE: CSSProperties = { ...PARAGRAPH_STYLE, gridColumn: '1 / -1' };
@@ -0,0 +1,95 @@
1
+ import { Classes, Tooltip } from '@blueprintjs/core';
2
+ import type { CSSProperties, ReactElement, RefObject } from 'react';
3
+
4
+ import type { EditorToolbarButton } from '../core/editorToolbar.ts';
5
+ import {
6
+ EDITOR_TOOLBAR_BUTTONS,
7
+ isEditorToolbarButtonAvailable,
8
+ } from '../core/editorToolbar.ts';
9
+
10
+ import { KeyCaps } from './KeyCaps.tsx';
11
+ import type { StructureEditorMode } from './editorChange.ts';
12
+ import { useToolbarHover } from './useToolbarHover.ts';
13
+
14
+ /** What {@link ToolbarTooltip} needs. */
15
+ export interface ToolbarTooltipProps {
16
+ /** The positioned element wrapping the editor. */
17
+ containerRef: RefObject<HTMLElement | null>;
18
+ /** Which editor the toolbar belongs to, since some buttons are greyed. */
19
+ mode: StructureEditorMode;
20
+ }
21
+
22
+ /**
23
+ * The name, key and purpose of the toolbar button under the pointer.
24
+ *
25
+ * The toolbar is a canvas, so the tooltip is anchored to an invisible box laid
26
+ * over the button rather than to the button itself; the box lets the pointer
27
+ * through, so the editor still sees every hover and click.
28
+ * @param props - See {@link ToolbarTooltipProps}.
29
+ * @returns The tooltip, or nothing before a button has been hovered.
30
+ */
31
+ export function ToolbarTooltip(
32
+ props: ToolbarTooltipProps,
33
+ ): ReactElement | null {
34
+ const { containerRef, mode } = props;
35
+ const hover = useToolbarHover(containerRef);
36
+ if (hover === null) return null;
37
+ const button = EDITOR_TOOLBAR_BUTTONS[hover.button];
38
+ if (button === undefined) return null;
39
+
40
+ return (
41
+ <Tooltip
42
+ isOpen={hover.open}
43
+ placement="right"
44
+ content={<ButtonTip button={button} mode={mode} />}
45
+ renderTarget={({ ref }) => (
46
+ <span ref={ref} aria-hidden style={{ ...TARGET_STYLE, ...hover.box }} />
47
+ )}
48
+ />
49
+ );
50
+ }
51
+
52
+ function ButtonTip(props: {
53
+ button: EditorToolbarButton;
54
+ mode: StructureEditorMode;
55
+ }): ReactElement {
56
+ const { button, mode } = props;
57
+ const available = isEditorToolbarButtonAvailable(button, mode);
58
+
59
+ return (
60
+ <div style={TIP_STYLE} data-testid="structure-editor-tooltip">
61
+ <div style={HEAD_STYLE}>
62
+ <strong>{button.name}</strong>
63
+ {button.keys === undefined ? null : <KeyCaps keys={button.keys} />}
64
+ </div>
65
+ <div>{button.description}</div>
66
+ {available ? null : (
67
+ <div className={Classes.TEXT_MUTED}>
68
+ {button.availability === 'reaction'
69
+ ? 'Only when drawing a reaction.'
70
+ : 'Not available in this editor.'}
71
+ </div>
72
+ )}
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const TARGET_STYLE: CSSProperties = {
78
+ position: 'absolute',
79
+ pointerEvents: 'none',
80
+ };
81
+
82
+ const TIP_STYLE: CSSProperties = {
83
+ display: 'flex',
84
+ flexDirection: 'column',
85
+ gap: 4,
86
+ maxWidth: 260,
87
+ lineHeight: 1.4,
88
+ };
89
+
90
+ const HEAD_STYLE: CSSProperties = {
91
+ display: 'flex',
92
+ alignItems: 'center',
93
+ justifyContent: 'space-between',
94
+ gap: 12,
95
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * What the structure editor reports after an edit.
3
+ *
4
+ * Only types are imported from react-ocl and openchemlib, so this module can be
5
+ * reached from the `react-cheminfo/structure` barrel without pulling either in.
6
+ */
7
+
8
+ import type { Molecule } from 'openchemlib';
9
+ import type {
10
+ CanvasEditorOnChangeMolecule,
11
+ CanvasEditorOnChangeReaction,
12
+ } from 'react-ocl';
13
+
14
+ /** What the editor draws: one structure, or a reaction with its arrow. */
15
+ export type StructureEditorMode = 'molecule' | 'reaction';
16
+
17
+ /** The notations every change carries, whichever the editor. */
18
+ interface StructureEditorNotations {
19
+ /**
20
+ * The idCode, with the coordinates the editor laid out written after a
21
+ * space. `splitIdCode` takes the two apart.
22
+ */
23
+ idCode: string;
24
+ /** The V2000 molfile of a molecule, or the RXN file of a reaction. */
25
+ molfile: string;
26
+ /** The SMILES of a molecule, or the reaction SMILES. */
27
+ smiles: string;
28
+ }
29
+
30
+ /** What a molecule editor holds at the moment it changed. */
31
+ export interface StructureEditorMoleculeChange extends StructureEditorNotations {
32
+ mode: 'molecule';
33
+ /**
34
+ * A copy of the drawn molecule, with its coordinates. It is taken when the
35
+ * edit happens, so it still describes that edit when it arrives after the
36
+ * debounce, and the caller may modify it without touching the canvas.
37
+ */
38
+ molecule: Molecule;
39
+ }
40
+
41
+ /** What a reaction editor holds at the moment it changed. */
42
+ export interface StructureEditorReactionChange extends StructureEditorNotations {
43
+ mode: 'reaction';
44
+ }
45
+
46
+ /** Everything the editor holds at the moment it changed. */
47
+ export type StructureEditorChange =
48
+ StructureEditorMoleculeChange | StructureEditorReactionChange;
49
+
50
+ /**
51
+ * Read a molecule editor's event out, while it still describes the edit.
52
+ * @param event - The event react-ocl passes to its callback.
53
+ * @returns The change, independent of the editor from then on.
54
+ */
55
+ export function moleculeChange(
56
+ event: CanvasEditorOnChangeMolecule,
57
+ ): StructureEditorMoleculeChange {
58
+ return {
59
+ mode: 'molecule',
60
+ idCode: event.getIdcode(),
61
+ molfile: event.getMolfile(),
62
+ smiles: event.getSmiles(),
63
+ molecule: event.getMolecule(),
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Read a reaction editor's event out, while it still describes the edit.
69
+ * @param event - The event react-ocl passes to its callback.
70
+ * @returns The change, independent of the editor from then on.
71
+ */
72
+ export function reactionChange(
73
+ event: CanvasEditorOnChangeReaction,
74
+ ): StructureEditorReactionChange {
75
+ return {
76
+ mode: 'reaction',
77
+ idCode: event.getIdcode(),
78
+ molfile: event.getRxn(),
79
+ smiles: event.getSmiles(),
80
+ };
81
+ }
@@ -13,10 +13,14 @@
13
13
  export type {
14
14
  StructureEditorChange,
15
15
  StructureEditorMode,
16
- } from './EditorCanvas.tsx';
16
+ StructureEditorMoleculeChange,
17
+ StructureEditorReactionChange,
18
+ } from './editorChange.ts';
17
19
  export type { StructureLabels, StructureProps } from './Structure.tsx';
18
20
  export { Structure } from './Structure.tsx';
19
21
  export type { StructureEditorProps } from './StructureEditor.tsx';
20
22
  export { StructureEditor } from './StructureEditor.tsx';
23
+ export type { StructureEditorHelpProps } from './StructureEditorHelp.tsx';
24
+ export { StructureEditorHelp } from './StructureEditorHelp.tsx';
21
25
  export type { ToolbarFloorOptions } from './useToolbarFloor.ts';
22
26
  export { useToolbarFloor } from './useToolbarFloor.ts';
@@ -3,6 +3,8 @@ import { useLayoutEffect, useRef } from 'react';
3
3
 
4
4
  import { toolbarFloorHeight } from '../core/toolbarFloor.ts';
5
5
 
6
+ import { watchToolbar } from './watchToolbar.ts';
7
+
6
8
  /** What {@link useToolbarFloor} needs. */
7
9
  export interface ToolbarFloorOptions {
8
10
  /**
@@ -38,62 +40,21 @@ export function useToolbarFloor(
38
40
  const container = containerRef.current;
39
41
  if (container === null) return;
40
42
 
41
- let toolbar: HTMLCanvasElement | null = null;
42
- let size: ResizeObserver | null = null;
43
-
44
- const attach = (): void => {
45
- const found = findToolbar(container);
46
- if (found === null) return;
47
- size?.disconnect();
48
- toolbar = found;
49
- size = new ResizeObserver(() => {
43
+ return watchToolbar(container, (toolbar) => {
44
+ const size = new ResizeObserver(() => {
50
45
  // offsetHeight, because a dialog opens under a scaling transform and a
51
46
  // measured rectangle would be that of the half drawn toolbar.
52
- const height = found.offsetHeight;
47
+ const height = toolbar.offsetHeight;
53
48
  // Zero is a toolbar being torn down or not yet laid out, never a
54
49
  // toolbar that needs no room: writing the fallback for it would undo a
55
50
  // correct floor and leave the palette cut off.
56
51
  if (height === 0) return;
57
52
  container.style.minHeight = `${toolbarFloorHeight(height, minHeight)}px`;
58
53
  });
59
- size.observe(found);
60
- };
61
-
62
- // The editor is imported lazily and builds itself asynchronously, so it is
63
- // almost never there on the first look, and how long it takes is a cold
64
- // module graph and a network away from anything this code can predict. So
65
- // the arrival is watched for rather than waited out: a deadline that
66
- // expires leaves the toolbar cut off for the life of the page, with
67
- // nothing on screen to say why.
68
- const arrivals = new MutationObserver(() => {
69
- if (toolbar?.isConnected === true) return;
70
- attach();
54
+ size.observe(toolbar);
55
+ return () => size.disconnect();
71
56
  });
72
- arrivals.observe(container, { childList: true, subtree: true });
73
- attach();
74
-
75
- return () => {
76
- arrivals.disconnect();
77
- size?.disconnect();
78
- };
79
57
  }, [minHeight, revision]);
80
58
 
81
59
  return containerRef;
82
60
  }
83
-
84
- /**
85
- * Find the toolbar canvas of the editor.
86
- *
87
- * The editor builds itself inside a shadow root and puts the toolbar there as
88
- * a direct child, the drawing canvas being nested deeper.
89
- * @param container - The element wrapping the editor.
90
- * @returns The toolbar canvas, or null while the editor has not drawn one.
91
- */
92
- function findToolbar(container: HTMLElement): HTMLCanvasElement | null {
93
- for (const element of container.querySelectorAll('*')) {
94
- for (const child of element.shadowRoot?.children ?? []) {
95
- if (child instanceof HTMLCanvasElement) return child;
96
- }
97
- }
98
- return null;
99
- }