@antv/infographic 0.2.13 → 0.2.15

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 (241) hide show
  1. package/README.md +12 -5
  2. package/README.zh-CN.md +12 -5
  3. package/dist/infographic.min.js +180 -171
  4. package/dist/infographic.min.js.map +1 -1
  5. package/esm/designs/structures/index.d.ts +1 -0
  6. package/esm/designs/structures/index.js +1 -0
  7. package/esm/designs/structures/relation-dagre-flow.js +4 -139
  8. package/esm/designs/structures/sequence-interaction.d.ts +54 -0
  9. package/esm/designs/structures/sequence-interaction.js +440 -0
  10. package/esm/designs/utils/geometry.d.ts +44 -0
  11. package/esm/designs/utils/geometry.js +244 -0
  12. package/esm/designs/utils/index.d.ts +1 -0
  13. package/esm/designs/utils/index.js +1 -0
  14. package/esm/editor/commands/UpdateOptions.d.ts +4 -4
  15. package/esm/editor/commands/UpdateOptions.js +6 -3
  16. package/esm/editor/editor.d.ts +5 -1
  17. package/esm/editor/editor.js +16 -5
  18. package/esm/editor/index.d.ts +1 -0
  19. package/esm/editor/index.js +1 -0
  20. package/esm/editor/interactions/brush-select.d.ts +0 -1
  21. package/esm/editor/interactions/brush-select.js +2 -10
  22. package/esm/editor/interactions/drag-canvas.d.ts +35 -0
  23. package/esm/editor/interactions/drag-canvas.js +161 -0
  24. package/esm/editor/interactions/drag-element.js +4 -3
  25. package/esm/editor/interactions/index.d.ts +1 -0
  26. package/esm/editor/interactions/index.js +1 -0
  27. package/esm/editor/interactions/zoom-wheel.d.ts +9 -0
  28. package/esm/editor/interactions/zoom-wheel.js +32 -15
  29. package/esm/editor/managers/index.d.ts +1 -0
  30. package/esm/editor/managers/index.js +1 -0
  31. package/esm/editor/managers/state.d.ts +4 -2
  32. package/esm/editor/managers/state.js +14 -13
  33. package/esm/editor/managers/sync-registry.d.ts +16 -0
  34. package/esm/editor/managers/sync-registry.js +51 -0
  35. package/esm/editor/plugins/{edit-bar/components → components}/button.js +1 -1
  36. package/esm/editor/plugins/{edit-bar/components → components}/color-picker.js +1 -1
  37. package/esm/editor/plugins/{edit-bar/components → components}/icons.d.ts +1 -0
  38. package/esm/editor/plugins/{edit-bar/components → components}/icons.js +1 -0
  39. package/esm/editor/plugins/{edit-bar/components → components}/popover.js +2 -2
  40. package/esm/editor/plugins/{edit-bar/components → components}/select.js +1 -1
  41. package/esm/editor/plugins/core-sync.d.ts +8 -0
  42. package/esm/editor/plugins/core-sync.js +30 -0
  43. package/esm/editor/plugins/edit-bar/edit-items/align-elements.js +1 -1
  44. package/esm/editor/plugins/edit-bar/edit-items/font-align.js +1 -1
  45. package/esm/editor/plugins/edit-bar/edit-items/font-color.js +1 -1
  46. package/esm/editor/plugins/edit-bar/edit-items/font-family.js +1 -1
  47. package/esm/editor/plugins/edit-bar/edit-items/font-size.js +1 -1
  48. package/esm/editor/plugins/edit-bar/edit-items/icon-color.js +1 -1
  49. package/esm/editor/plugins/edit-bar/index.d.ts +2 -2
  50. package/esm/editor/plugins/edit-bar/index.js +1 -1
  51. package/esm/editor/plugins/index.d.ts +2 -0
  52. package/esm/editor/plugins/index.js +2 -0
  53. package/esm/editor/plugins/reset-viewbox.d.ts +33 -0
  54. package/esm/editor/plugins/reset-viewbox.js +186 -0
  55. package/esm/editor/types/editor.d.ts +14 -0
  56. package/esm/editor/types/index.d.ts +1 -0
  57. package/esm/editor/types/interaction.d.ts +9 -0
  58. package/esm/editor/types/state.d.ts +4 -2
  59. package/esm/editor/types/sync.d.ts +27 -0
  60. package/esm/editor/types/sync.js +1 -0
  61. package/esm/editor/utils/data.js +3 -1
  62. package/esm/editor/utils/event.d.ts +1 -0
  63. package/esm/editor/utils/event.js +8 -0
  64. package/esm/editor/utils/index.d.ts +1 -0
  65. package/esm/editor/utils/index.js +1 -0
  66. package/esm/editor/utils/object.d.ts +15 -0
  67. package/esm/editor/utils/object.js +77 -0
  68. package/esm/index.d.ts +4 -3
  69. package/esm/index.js +3 -2
  70. package/esm/options/types.d.ts +7 -0
  71. package/esm/runtime/Infographic.js +20 -7
  72. package/esm/runtime/options.js +7 -2
  73. package/esm/syntax/index.js +40 -20
  74. package/esm/syntax/relations.js +26 -2
  75. package/esm/syntax/schema.js +1 -0
  76. package/esm/templates/built-in.js +27 -2
  77. package/esm/templates/sequence-interaction.d.ts +2 -0
  78. package/esm/templates/sequence-interaction.js +76 -0
  79. package/esm/types/data.d.ts +1 -0
  80. package/esm/utils/index.d.ts +1 -0
  81. package/esm/utils/index.js +1 -0
  82. package/esm/utils/measure-text.js +40 -9
  83. package/esm/utils/padding.d.ts +1 -0
  84. package/esm/utils/padding.js +6 -2
  85. package/esm/utils/types.d.ts +16 -0
  86. package/esm/utils/types.js +12 -0
  87. package/esm/version.d.ts +1 -1
  88. package/esm/version.js +1 -1
  89. package/lib/designs/structures/index.d.ts +1 -0
  90. package/lib/designs/structures/index.js +1 -0
  91. package/lib/designs/structures/relation-dagre-flow.js +5 -140
  92. package/lib/designs/structures/sequence-interaction.d.ts +54 -0
  93. package/lib/designs/structures/sequence-interaction.js +444 -0
  94. package/lib/designs/utils/geometry.d.ts +44 -0
  95. package/lib/designs/utils/geometry.js +256 -0
  96. package/lib/designs/utils/index.d.ts +1 -0
  97. package/lib/designs/utils/index.js +1 -0
  98. package/lib/editor/commands/UpdateOptions.d.ts +4 -4
  99. package/lib/editor/commands/UpdateOptions.js +6 -3
  100. package/lib/editor/editor.d.ts +5 -1
  101. package/lib/editor/editor.js +16 -5
  102. package/lib/editor/index.d.ts +1 -0
  103. package/lib/editor/index.js +1 -0
  104. package/lib/editor/interactions/brush-select.d.ts +0 -1
  105. package/lib/editor/interactions/brush-select.js +1 -9
  106. package/lib/editor/interactions/drag-canvas.d.ts +35 -0
  107. package/lib/editor/interactions/drag-canvas.js +165 -0
  108. package/lib/editor/interactions/drag-element.js +4 -3
  109. package/lib/editor/interactions/index.d.ts +1 -0
  110. package/lib/editor/interactions/index.js +3 -1
  111. package/lib/editor/interactions/zoom-wheel.d.ts +9 -0
  112. package/lib/editor/interactions/zoom-wheel.js +32 -15
  113. package/lib/editor/managers/index.d.ts +1 -0
  114. package/lib/editor/managers/index.js +1 -0
  115. package/lib/editor/managers/state.d.ts +4 -2
  116. package/lib/editor/managers/state.js +12 -11
  117. package/lib/editor/managers/sync-registry.d.ts +16 -0
  118. package/lib/editor/managers/sync-registry.js +55 -0
  119. package/lib/editor/plugins/{edit-bar/components → components}/button.js +1 -1
  120. package/lib/editor/plugins/{edit-bar/components → components}/color-picker.js +1 -1
  121. package/lib/editor/plugins/{edit-bar/components → components}/icons.d.ts +1 -0
  122. package/lib/editor/plugins/{edit-bar/components → components}/icons.js +2 -1
  123. package/lib/editor/plugins/{edit-bar/components → components}/popover.js +2 -2
  124. package/lib/editor/plugins/{edit-bar/components → components}/select.js +1 -1
  125. package/lib/editor/plugins/core-sync.d.ts +8 -0
  126. package/lib/editor/plugins/core-sync.js +34 -0
  127. package/lib/editor/plugins/edit-bar/edit-items/align-elements.js +1 -1
  128. package/lib/editor/plugins/edit-bar/edit-items/font-align.js +1 -1
  129. package/lib/editor/plugins/edit-bar/edit-items/font-color.js +1 -1
  130. package/lib/editor/plugins/edit-bar/edit-items/font-family.js +1 -1
  131. package/lib/editor/plugins/edit-bar/edit-items/font-size.js +1 -1
  132. package/lib/editor/plugins/edit-bar/edit-items/icon-color.js +1 -1
  133. package/lib/editor/plugins/edit-bar/index.d.ts +2 -2
  134. package/lib/editor/plugins/edit-bar/index.js +1 -1
  135. package/lib/editor/plugins/index.d.ts +2 -0
  136. package/lib/editor/plugins/index.js +5 -1
  137. package/lib/editor/plugins/reset-viewbox.d.ts +33 -0
  138. package/lib/editor/plugins/reset-viewbox.js +190 -0
  139. package/lib/editor/types/editor.d.ts +14 -0
  140. package/lib/editor/types/index.d.ts +1 -0
  141. package/lib/editor/types/interaction.d.ts +9 -0
  142. package/lib/editor/types/state.d.ts +4 -2
  143. package/lib/editor/types/sync.d.ts +27 -0
  144. package/lib/editor/types/sync.js +2 -0
  145. package/lib/editor/utils/data.js +3 -1
  146. package/lib/editor/utils/event.d.ts +1 -0
  147. package/lib/editor/utils/event.js +9 -0
  148. package/lib/editor/utils/index.d.ts +1 -0
  149. package/lib/editor/utils/index.js +1 -0
  150. package/lib/editor/utils/object.d.ts +15 -0
  151. package/lib/editor/utils/object.js +80 -0
  152. package/lib/index.d.ts +4 -3
  153. package/lib/index.js +9 -2
  154. package/lib/options/types.d.ts +7 -0
  155. package/lib/runtime/Infographic.js +19 -6
  156. package/lib/runtime/options.js +6 -1
  157. package/lib/syntax/index.js +40 -20
  158. package/lib/syntax/relations.js +26 -2
  159. package/lib/syntax/schema.js +1 -0
  160. package/lib/templates/built-in.js +27 -2
  161. package/lib/templates/sequence-interaction.d.ts +2 -0
  162. package/lib/templates/sequence-interaction.js +79 -0
  163. package/lib/types/data.d.ts +1 -0
  164. package/lib/utils/index.d.ts +1 -0
  165. package/lib/utils/index.js +1 -0
  166. package/lib/utils/measure-text.js +39 -8
  167. package/lib/utils/padding.d.ts +1 -0
  168. package/lib/utils/padding.js +7 -2
  169. package/lib/utils/types.d.ts +16 -0
  170. package/lib/utils/types.js +13 -0
  171. package/lib/version.d.ts +1 -1
  172. package/lib/version.js +1 -1
  173. package/package.json +1 -1
  174. package/src/designs/structures/index.ts +1 -0
  175. package/src/designs/structures/relation-dagre-flow.tsx +14 -178
  176. package/src/designs/structures/sequence-interaction.tsx +885 -0
  177. package/src/designs/utils/geometry.tsx +315 -0
  178. package/src/designs/utils/index.ts +1 -0
  179. package/src/editor/commands/UpdateOptions.ts +11 -6
  180. package/src/editor/editor.ts +26 -5
  181. package/src/editor/index.ts +1 -0
  182. package/src/editor/interactions/brush-select.ts +2 -8
  183. package/src/editor/interactions/drag-canvas.ts +203 -0
  184. package/src/editor/interactions/drag-element.ts +5 -4
  185. package/src/editor/interactions/index.ts +1 -0
  186. package/src/editor/interactions/zoom-wheel.ts +49 -13
  187. package/src/editor/managers/index.ts +1 -0
  188. package/src/editor/managers/state.ts +21 -21
  189. package/src/editor/managers/sync-registry.ts +66 -0
  190. package/src/editor/plugins/{edit-bar/components → components}/button.ts +1 -1
  191. package/src/editor/plugins/{edit-bar/components → components}/color-picker.ts +1 -1
  192. package/src/editor/plugins/{edit-bar/components → components}/icons.ts +4 -0
  193. package/src/editor/plugins/{edit-bar/components → components}/popover.ts +2 -2
  194. package/src/editor/plugins/{edit-bar/components → components}/select.ts +1 -1
  195. package/src/editor/plugins/core-sync.ts +44 -0
  196. package/src/editor/plugins/edit-bar/edit-items/align-elements.ts +2 -2
  197. package/src/editor/plugins/edit-bar/edit-items/font-align.ts +1 -1
  198. package/src/editor/plugins/edit-bar/edit-items/font-color.ts +1 -1
  199. package/src/editor/plugins/edit-bar/edit-items/font-family.ts +1 -1
  200. package/src/editor/plugins/edit-bar/edit-items/font-size.ts +3 -3
  201. package/src/editor/plugins/edit-bar/edit-items/icon-color.ts +1 -1
  202. package/src/editor/plugins/edit-bar/index.ts +2 -2
  203. package/src/editor/plugins/index.ts +2 -0
  204. package/src/editor/plugins/reset-viewbox.ts +258 -0
  205. package/src/editor/types/editor.ts +18 -0
  206. package/src/editor/types/index.ts +1 -0
  207. package/src/editor/types/interaction.ts +31 -0
  208. package/src/editor/types/state.ts +14 -2
  209. package/src/editor/types/sync.ts +33 -0
  210. package/src/editor/utils/data.ts +2 -1
  211. package/src/editor/utils/event.ts +7 -0
  212. package/src/editor/utils/index.ts +1 -0
  213. package/src/editor/utils/object.ts +116 -0
  214. package/src/index.ts +26 -2
  215. package/src/options/types.ts +11 -0
  216. package/src/runtime/Infographic.tsx +27 -17
  217. package/src/runtime/options.ts +8 -1
  218. package/src/syntax/index.ts +51 -18
  219. package/src/syntax/relations.ts +29 -2
  220. package/src/syntax/schema.ts +1 -0
  221. package/src/templates/built-in.ts +30 -0
  222. package/src/templates/sequence-interaction.ts +101 -0
  223. package/src/types/data.ts +1 -0
  224. package/src/utils/index.ts +1 -0
  225. package/src/utils/measure-text.ts +41 -9
  226. package/src/utils/padding.ts +10 -2
  227. package/src/utils/types.ts +61 -0
  228. package/src/version.ts +1 -1
  229. /package/esm/editor/plugins/{edit-bar/components → components}/button.d.ts +0 -0
  230. /package/esm/editor/plugins/{edit-bar/components → components}/color-picker.d.ts +0 -0
  231. /package/esm/editor/plugins/{edit-bar/components → components}/index.d.ts +0 -0
  232. /package/esm/editor/plugins/{edit-bar/components → components}/index.js +0 -0
  233. /package/esm/editor/plugins/{edit-bar/components → components}/popover.d.ts +0 -0
  234. /package/esm/editor/plugins/{edit-bar/components → components}/select.d.ts +0 -0
  235. /package/lib/editor/plugins/{edit-bar/components → components}/button.d.ts +0 -0
  236. /package/lib/editor/plugins/{edit-bar/components → components}/color-picker.d.ts +0 -0
  237. /package/lib/editor/plugins/{edit-bar/components → components}/index.d.ts +0 -0
  238. /package/lib/editor/plugins/{edit-bar/components → components}/index.js +0 -0
  239. /package/lib/editor/plugins/{edit-bar/components → components}/popover.d.ts +0 -0
  240. /package/lib/editor/plugins/{edit-bar/components → components}/select.d.ts +0 -0
  241. /package/src/editor/plugins/{edit-bar/components → components}/index.ts +0 -0
@@ -0,0 +1,258 @@
1
+ import { COMPONENT_ROLE } from '../../constants';
2
+ import {
3
+ getBoundViewBox,
4
+ getViewBox,
5
+ injectStyleOnce,
6
+ parsePadding,
7
+ setElementRole,
8
+ viewBoxToString,
9
+ } from '../../utils';
10
+ import { UpdateOptionsCommand } from '../commands';
11
+ import type { IPlugin, PluginInitOptions } from '../types';
12
+ import { Plugin } from './base';
13
+ import { IconButton } from './components';
14
+ import { RESET_ICON } from './components/icons';
15
+
16
+ const MARGIN_X = 25;
17
+ const MARGIN_Y = 25;
18
+ const BUTTON_SIZE = 40;
19
+ export interface ResetViewBoxOptions {
20
+ style?: Partial<CSSStyleDeclaration>;
21
+ className?: string;
22
+ getContainer?: HTMLElement | (() => HTMLElement);
23
+ }
24
+
25
+ export class ResetViewBox extends Plugin implements IPlugin {
26
+ name = 'reset-viewBox';
27
+
28
+ private originViewBox!: string;
29
+ private resetButton?: HTMLButtonElement;
30
+ private viewBoxChanged = false;
31
+ private unregisterSync?: () => void;
32
+
33
+ constructor(private options?: ResetViewBoxOptions) {
34
+ super();
35
+ }
36
+
37
+ init(options: PluginInitOptions) {
38
+ super.init(options);
39
+
40
+ // Initialize originViewBox
41
+ this.ensureButtonStyle();
42
+ this.updateOriginViewBox();
43
+
44
+ this.unregisterSync = this.editor.registerSync(
45
+ 'viewBox',
46
+ this.handleViewBoxChange,
47
+ );
48
+ window.addEventListener('resize', this.handleResize);
49
+ }
50
+
51
+ destroy(): void {
52
+ this.unregisterSync?.();
53
+ window.removeEventListener('resize', this.handleResize);
54
+ this.removeButton();
55
+ }
56
+
57
+ private handleResize = () => {
58
+ this.updateOriginViewBox();
59
+ this.updatePosition();
60
+ };
61
+
62
+ private updateOriginViewBox() {
63
+ const svg = this.editor.getDocument();
64
+ // In Node env or before render, fallback to current viewBox attribute
65
+ if (!svg.getBBox) {
66
+ this.originViewBox = viewBoxToString(getViewBox(svg));
67
+ return;
68
+ }
69
+
70
+ // In Browser, calculate fit
71
+ const { padding } = this.state.getOptions();
72
+ this.originViewBox = getBoundViewBox(svg, parsePadding(padding));
73
+ }
74
+
75
+ private handleViewBoxChange = (viewBox?: string) => {
76
+ const svg = this.editor.getDocument();
77
+
78
+ this.viewBoxChanged = viewBox !== this.originViewBox;
79
+
80
+ if (!this.viewBoxChanged) {
81
+ if (this.resetButton) this.hideButton(this.resetButton);
82
+ return;
83
+ }
84
+ const button = this.getOrCreateResetButton();
85
+
86
+ this.placeButton(button, svg);
87
+ this.showButton(button);
88
+ };
89
+
90
+ protected getOrCreateResetButton = () => {
91
+ if (this.resetButton) return this.resetButton;
92
+
93
+ const { style, className } = this.options || {};
94
+
95
+ const button = IconButton({
96
+ icon: RESET_ICON,
97
+ onClick: this.resetViewBox,
98
+ });
99
+
100
+ button.classList.add(RESET_BUTTON_CLASS);
101
+
102
+ if (className) {
103
+ button.classList.add(className);
104
+ }
105
+
106
+ if (style) {
107
+ Object.assign(button.style, style);
108
+ }
109
+
110
+ setElementRole(button, COMPONENT_ROLE);
111
+
112
+ this.resetButton = button;
113
+
114
+ const { getContainer } = this.options || {};
115
+ const resolvedContainer =
116
+ typeof getContainer === 'function' ? getContainer() : getContainer;
117
+ const containerParent = resolvedContainer ?? document.body;
118
+
119
+ containerParent?.appendChild(button);
120
+
121
+ return button;
122
+ };
123
+
124
+ /** Get CSS transform scale of an element */
125
+ private getTransformScale = (
126
+ element: HTMLElement,
127
+ ): { scaleX: number; scaleY: number } => {
128
+ const rect = element.getBoundingClientRect();
129
+ const scaleX =
130
+ element.offsetWidth > 0 ? rect.width / element.offsetWidth : 1;
131
+ const scaleY =
132
+ element.offsetHeight > 0 ? rect.height / element.offsetHeight : 1;
133
+ return { scaleX, scaleY };
134
+ };
135
+
136
+ /** Find the nearest stable overflow container */
137
+ private findStableContainer = (svg: SVGSVGElement): HTMLElement | null => {
138
+ let current: Element | null = svg.parentElement;
139
+ while (current) {
140
+ if (current instanceof HTMLElement) {
141
+ const style = window.getComputedStyle(current);
142
+ // Look for overflow container or positioned element as stable reference
143
+ if (
144
+ (style.overflow && style.overflow !== 'visible') ||
145
+ (style.overflowX && style.overflowX !== 'visible') ||
146
+ (style.overflowY && style.overflowY !== 'visible')
147
+ ) {
148
+ return current;
149
+ }
150
+ }
151
+ current = current.parentElement;
152
+ }
153
+ return null;
154
+ };
155
+
156
+ private placeButton = (button: HTMLButtonElement, svg: SVGSVGElement) => {
157
+ if (!svg.isConnected) return;
158
+ const svgRect = svg.getBoundingClientRect();
159
+ const offsetParent =
160
+ (button.offsetParent as HTMLElement | null) ??
161
+ (document.documentElement as HTMLElement);
162
+
163
+ // Use stable container bounds to clamp button position when SVG overflows
164
+ const stableContainer = this.findStableContainer(svg);
165
+ const containerRect = stableContainer?.getBoundingClientRect();
166
+
167
+ // Prefer SVG bounds, but clamp to container if SVG overflows
168
+ const effectiveRect = containerRect
169
+ ? {
170
+ right: Math.min(svgRect.right, containerRect.right),
171
+ bottom: Math.min(svgRect.bottom, containerRect.bottom),
172
+ }
173
+ : svgRect;
174
+
175
+ if (
176
+ offsetParent === document.body ||
177
+ offsetParent === document.documentElement
178
+ ) {
179
+ const scrollX = window.scrollX || document.documentElement.scrollLeft;
180
+ const scrollY = window.scrollY || document.documentElement.scrollTop;
181
+
182
+ const left = scrollX + effectiveRect.right - MARGIN_X - BUTTON_SIZE;
183
+ const top = scrollY + effectiveRect.bottom - MARGIN_Y - BUTTON_SIZE;
184
+
185
+ button.style.left = `${left}px`;
186
+ button.style.top = `${top}px`;
187
+ return;
188
+ }
189
+
190
+ const parentRect = offsetParent.getBoundingClientRect();
191
+
192
+ // Compensate for parent transform scale
193
+ const { scaleX, scaleY } = this.getTransformScale(offsetParent);
194
+
195
+ // Convert to offsetParent local coordinates
196
+ const left =
197
+ (effectiveRect.right - parentRect.left) / scaleX - MARGIN_X - BUTTON_SIZE;
198
+ const top =
199
+ (effectiveRect.bottom - parentRect.top) / scaleY - MARGIN_Y - BUTTON_SIZE;
200
+
201
+ button.style.left = `${left}px`;
202
+ button.style.top = `${top}px`;
203
+ };
204
+ private updatePosition = () => {
205
+ if (this.resetButton && this.viewBoxChanged) {
206
+ this.placeButton(this.resetButton, this.editor.getDocument());
207
+ }
208
+ };
209
+
210
+ private resetViewBox = () => {
211
+ const command = new UpdateOptionsCommand({
212
+ viewBox: this.originViewBox,
213
+ });
214
+ void this.commander.execute(command);
215
+ };
216
+
217
+ private showButton = (button: HTMLButtonElement) => {
218
+ button.style.display = 'flex';
219
+ button.style.visibility = 'visible';
220
+ };
221
+
222
+ private hideButton = (button: HTMLButtonElement) => {
223
+ button.style.display = 'none';
224
+ };
225
+
226
+ private removeButton = () => {
227
+ this.viewBoxChanged = false;
228
+ this.resetButton?.remove();
229
+ this.resetButton = undefined;
230
+ };
231
+
232
+ private ensureButtonStyle() {
233
+ injectStyleOnce(
234
+ RESET_BUTTON_STYLE_ID,
235
+ `
236
+ button.${RESET_BUTTON_CLASS} {
237
+ visibility: hidden;
238
+ position: absolute;
239
+ display: flex;
240
+ justify-content: center;
241
+ align-items: center;
242
+ width: ${BUTTON_SIZE}px;
243
+ height: ${BUTTON_SIZE}px;
244
+ border-radius: 50%;
245
+ padding: 4px;
246
+ background-color: #fff;
247
+ border: 1px solid #e5e7eb;
248
+ z-index: 1000;
249
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
250
+ cursor: pointer;
251
+ }
252
+ `,
253
+ );
254
+ }
255
+ }
256
+
257
+ const RESET_BUTTON_CLASS = 'infographic-reset-viewbox-btn';
258
+ const RESET_BUTTON_STYLE_ID = 'infographic-reset-viewbox-btn-style';
@@ -1,4 +1,22 @@
1
+ import type { InfographicOptionPath } from '../../options';
2
+ import type { ICommandManager } from './command';
3
+ import type { IInteractionManager } from './interaction';
4
+ import type { IPluginManager } from './plugin';
5
+ import type { IStateManager } from './state';
6
+ import type { ISyncRegistry, SyncHandler } from './sync';
7
+
1
8
  export interface IEditor {
9
+ commander: ICommandManager;
10
+ interaction: IInteractionManager;
11
+ plugin: IPluginManager;
12
+ state: IStateManager;
13
+ syncRegistry: ISyncRegistry;
14
+
2
15
  getDocument(): SVGSVGElement;
16
+ registerSync(
17
+ path: InfographicOptionPath | (string & {}),
18
+ handler: SyncHandler,
19
+ options?: { immediate?: boolean },
20
+ ): () => void;
3
21
  destroy(): void;
4
22
  }
@@ -5,3 +5,4 @@ export type * from './plugin';
5
5
  export type * from './selection';
6
6
  export type * from './shape';
7
7
  export type * from './state';
8
+ export type * from './sync';
@@ -4,6 +4,32 @@ import type { IEditor } from './editor';
4
4
  import type { Selection } from './selection';
5
5
  import type { IStateManager } from './state';
6
6
 
7
+ /**
8
+ * Union type of common key codes, using (string & {}) trick to provide completion suggestions without limiting specific strings
9
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
10
+ */
11
+ export type KeyCode =
12
+ | 'Space'
13
+ | 'ShiftLeft'
14
+ | 'ShiftRight'
15
+ | 'ControlLeft'
16
+ | 'ControlRight'
17
+ | 'AltLeft'
18
+ | 'AltRight'
19
+ | 'MetaLeft'
20
+ | 'MetaRight'
21
+ | 'CapsLock'
22
+ | 'Tab'
23
+ | 'Enter'
24
+ | 'Escape'
25
+ | 'ArrowUp'
26
+ | 'ArrowDown'
27
+ | 'ArrowLeft'
28
+ | 'ArrowRight'
29
+ | `Key${string}`
30
+ | `Digit${number}`
31
+ | (string & {});
32
+
7
33
  export interface IInteraction {
8
34
  name: string;
9
35
  init(options: InteractionInitOptions): void;
@@ -21,6 +47,11 @@ export interface SelectionChangePayload {
21
47
  mode: SelectMode;
22
48
  }
23
49
 
50
+ export interface viewBoxChangePayload {
51
+ type: 'viewBox:change';
52
+ viewBox: string | undefined;
53
+ }
54
+
24
55
  export interface IInteractionManager {
25
56
  isActive(): boolean;
26
57
  select(items: Selection, mode: SelectMode): void;
@@ -1,4 +1,7 @@
1
- import type { ParsedInfographicOptions } from '../../options';
1
+ import type {
2
+ ParsedInfographicOptions,
3
+ UpdatableInfographicOptions,
4
+ } from '../../options';
2
5
  import type { Element, IEventEmitter, ItemDatum } from '../../types';
3
6
  import type { ICommandManager } from './command';
4
7
  import type { IEditor } from './editor';
@@ -24,7 +27,16 @@ export interface IStateManager {
24
27
  removeItemDatum(indexes: number[], count?: number): void;
25
28
  updateData(key: string, value: any): void;
26
29
  updateElement(element: Element, props: Partial<ElementProps>): void;
27
- updateOptions(options: ParsedInfographicOptions): void;
30
+ updateOptions(
31
+ options: UpdatableInfographicOptions,
32
+ // Configuration for this update execution
33
+ execOptions?: {
34
+ // Whether to bubble up notifications to parent paths.
35
+ // Enabling this might duplicate objects and impact performance.
36
+ // Default is false.
37
+ bubbleUp?: boolean;
38
+ },
39
+ ): void;
28
40
  getOptions(): ParsedInfographicOptions;
29
41
  destroy(): void;
30
42
  }
@@ -0,0 +1,33 @@
1
+ import type { InfographicOptionPath } from '../../options';
2
+
3
+ /**
4
+ * Sync callback
5
+ * @param newValue The new value after modification
6
+ * @param oldValue The old value before modification. Note: This value is undefined when triggered by parent path bubbling notification (i.e., listening to a non-leaf node).
7
+ */
8
+ export type SyncHandler = (newValue: any, oldValue: any) => void;
9
+
10
+ export interface ISyncRegistry {
11
+ /**
12
+ * Register synchronization logic
13
+ * @param path Configuration path, such as 'design.background'
14
+ * @param handler Sync callback
15
+ * @param options.immediate Whether to execute immediately (used for view initialization)
16
+ * @returns unregister function
17
+ */
18
+ register(
19
+ path: InfographicOptionPath | (string & {}),
20
+ handler: SyncHandler,
21
+ options?: { immediate?: boolean },
22
+ ): () => void;
23
+
24
+ /**
25
+ * Trigger synchronization (usually called by StateManager)
26
+ * @param path Configuration path
27
+ * @param newVal New value
28
+ * @param oldVal Old value
29
+ */
30
+ trigger(path: string, newVal: any, oldVal: any): void;
31
+
32
+ destroy(): void;
33
+ }
@@ -8,8 +8,9 @@ export function getChildrenDataByIndexes(
8
8
  data: ParsedData,
9
9
  indexes: number[],
10
10
  ): ItemDatum[] {
11
- if (indexes.length === 0) return data.data;
11
+ if (indexes.length === 0) return data.items;
12
12
  const datum = getDatumByIndexes(data, indexes);
13
+ if (datum == null) return [];
13
14
  datum.children ||= [];
14
15
  return datum.children;
15
16
  }
@@ -90,3 +90,10 @@ const getIconEventTarget = (element: SVGElement): IconElement | null => {
90
90
 
91
91
  return null;
92
92
  };
93
+
94
+ export function isTextSelectionTarget(target: EventTarget | null) {
95
+ if (!(target instanceof HTMLElement)) return false;
96
+ if (target.isContentEditable) return true;
97
+ const tag = target.tagName.toLowerCase();
98
+ return tag === 'input' || tag === 'textarea';
99
+ }
@@ -5,3 +5,4 @@ export * from './element';
5
5
  export * from './event';
6
6
  export * from './extension';
7
7
  export * from './hotkey';
8
+ export * from './object';
@@ -0,0 +1,116 @@
1
+ import { cloneDeep, isEqual, isPlainObject } from 'lodash-es';
2
+
3
+ export interface ApplyOptionUpdatesOptions {
4
+ /** Whether to notify parent paths of changes (bubbling) */
5
+ bubbleUp?: boolean;
6
+ /** Callback triggered whenever a property value changes */
7
+ collector?: (path: string, newVal: any, oldVal: any) => void;
8
+ }
9
+
10
+ /**
11
+ * Recursively applies properties from 'source' to 'target' and collects changes.
12
+ *
13
+ * @param target - The object to be updated
14
+ * @param source - The source object containing partial updates
15
+ * @param basePath - Current path prefix for nested properties
16
+ * @param options - Configuration options
17
+ */
18
+ export function applyOptionUpdates(
19
+ target: any,
20
+ source: any,
21
+ basePath: string = '',
22
+ options?: ApplyOptionUpdatesOptions,
23
+ ): void {
24
+ const { bubbleUp = false, collector } = options ?? {};
25
+
26
+ const hasChange = applyOptionUpdatesInternal(
27
+ target,
28
+ source,
29
+ basePath,
30
+ collector,
31
+ bubbleUp,
32
+ );
33
+
34
+ if (basePath === '' && hasChange && bubbleUp && collector) {
35
+ collector('', cloneDeep(target), undefined);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Internal recursive function.
41
+ * Returns true if any change occurred within this branch (or its children).
42
+ */
43
+ function applyOptionUpdatesInternal(
44
+ target: any,
45
+ source: any,
46
+ basePath: string,
47
+ collector: ((path: string, newVal: any, oldVal: any) => void) | undefined,
48
+ bubbleUp: boolean,
49
+ ): boolean {
50
+ let hasChange = false;
51
+
52
+ Object.keys(source).forEach((key) => {
53
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
54
+ return;
55
+ }
56
+
57
+ const fullPath = basePath ? `${basePath}.${key}` : key;
58
+ const updateValue = source[key];
59
+ const oldValue = target[key];
60
+ let childChanged = false;
61
+
62
+ if (updateValue === undefined) {
63
+ // Handle deletion: Only delete and notify if the key actually exists
64
+ if (key in target) {
65
+ delete target[key];
66
+ collector?.(fullPath, undefined, oldValue);
67
+ childChanged = true;
68
+ }
69
+ } else if (isPlainObject(updateValue)) {
70
+ // Handle nested object
71
+ const oldValueIsObject = isPlainObject(target[key]);
72
+ if (!oldValueIsObject) {
73
+ target[key] = {};
74
+ }
75
+
76
+ const grandChildChanged = applyOptionUpdatesInternal(
77
+ target[key],
78
+ updateValue,
79
+ fullPath,
80
+ collector,
81
+ bubbleUp,
82
+ );
83
+
84
+ if (!oldValueIsObject) {
85
+ // Overwriting a primitive with an object is always a change.
86
+ childChanged = true;
87
+ // If the object was empty (grandChildChanged is false), we still need to report it.
88
+ if (!grandChildChanged) {
89
+ collector?.(fullPath, target[key], oldValue);
90
+ }
91
+ } else {
92
+ childChanged = grandChildChanged;
93
+ }
94
+ } else {
95
+ // Handle primitive update
96
+ target[key] = updateValue;
97
+ if (!isEqual(updateValue, oldValue)) {
98
+ collector?.(fullPath, updateValue, oldValue);
99
+ childChanged = true;
100
+ }
101
+ }
102
+
103
+ if (childChanged) {
104
+ hasChange = true;
105
+ }
106
+ });
107
+
108
+ // Bubbling: Notify if any child changed in this branch
109
+ // The recursion naturally ensures this happens in "deepest-first" (post-order) sequence.
110
+ if (hasChange && bubbleUp && basePath !== '') {
111
+ // Current target is now fully updated for this scope.
112
+ collector?.(basePath, cloneDeep(target), undefined);
113
+ }
114
+
115
+ return hasChange;
116
+ }
package/src/index.ts CHANGED
@@ -1,16 +1,23 @@
1
1
  export * from './designs';
2
2
  export { getItemProps, getThemeColors } from './designs/utils';
3
+ export {
4
+ BatchCommand,
5
+ UpdateElementCommand,
6
+ UpdateOptionsCommand,
7
+ UpdateTextCommand,
8
+ } from './editor/commands';
3
9
  export {
4
10
  BrushSelect,
5
11
  ClickSelect,
6
12
  DblClickEditText,
13
+ DragCanvas,
7
14
  DragElement,
8
15
  HotkeyHistory,
9
16
  Interaction,
10
17
  SelectHighlight,
11
18
  ZoomWheel,
12
19
  } from './editor/interactions';
13
- export { EditBar, Plugin, ResizeElement } from './editor/plugins';
20
+ export { EditBar, Plugin, ResetViewBox, ResizeElement } from './editor/plugins';
14
21
  export { exportToSVG } from './exporter';
15
22
  export {
16
23
  Defs,
@@ -51,7 +58,24 @@ export { getTheme, getThemes, registerTheme } from './themes';
51
58
  export { parseSVG, setFontExtendFactor } from './utils';
52
59
  export { VERSION } from './version';
53
60
 
54
- export type { EditBarOptions } from './editor';
61
+ export type {
62
+ EditBarOptions,
63
+ ICommand,
64
+ ICommandManager,
65
+ IEditor,
66
+ IInteraction,
67
+ IInteractionManager,
68
+ IPlugin,
69
+ IPluginManager,
70
+ IStateManager,
71
+ ISyncRegistry,
72
+ InteractionInitOptions,
73
+ KeyCode,
74
+ PluginInitOptions,
75
+ SelectMode,
76
+ Selection,
77
+ SyncHandler,
78
+ } from './editor';
55
79
  export type {
56
80
  ExportOptions,
57
81
  PNGExportOptions,
@@ -2,6 +2,7 @@ import type { DesignOptions, ParsedDesignsOptions } from '../designs';
2
2
  import type { ElementProps, IInteraction, IPlugin } from '../editor';
3
3
  import type { ThemeConfig } from '../themes';
4
4
  import type { Data, Padding, ParsedData } from '../types';
5
+ import type { Path } from '../utils';
5
6
 
6
7
  export interface InfographicOptions {
7
8
  /** 容器,可以是选择器或者 HTMLElement */
@@ -54,6 +55,10 @@ export interface ParsedInfographicOptions {
54
55
  shapes?: ElementProps[];
55
56
  }
56
57
 
58
+ export type UpdatableInfographicOptions = Partial<
59
+ Omit<ParsedInfographicOptions, 'container'>
60
+ >;
61
+
57
62
  interface SVGOptions {
58
63
  style?: Record<string, string | number>;
59
64
  attributes?: Record<string, string | number | boolean>;
@@ -62,3 +67,9 @@ interface SVGOptions {
62
67
  /** 是否启用背景 */
63
68
  background?: boolean;
64
69
  }
70
+
71
+ /**
72
+ * All valid property paths for Infographic Options.
73
+ * Use this to validate paths in SyncRegistry and other places.
74
+ */
75
+ export type InfographicOptionPath = Path<UpdatableInfographicOptions>;
@@ -11,7 +11,7 @@ import {
11
11
  ParsedInfographicOptions,
12
12
  parseOptions,
13
13
  } from '../options';
14
- import { Renderer } from '../renderer';
14
+ import { DEFAULT_FONT, Renderer, setDefaultFont } from '../renderer';
15
15
  import { waitForSvgLoads } from '../resource';
16
16
  import { parseSyntax, type SyntaxError } from '../syntax';
17
17
  import { IEventEmitter } from '../types';
@@ -133,29 +133,39 @@ export class Infographic {
133
133
  * Compose the SVG template
134
134
  */
135
135
  compose(parsedOptions: ParsedInfographicOptions): SVGSVGElement {
136
- const { design, data } = parsedOptions;
136
+ const { design, data, themeConfig } = parsedOptions;
137
137
  const { title, item, items, structure } = design;
138
138
  const { component: Structure, props: structureProps } = structure;
139
139
  const Title = title.component;
140
140
  const Item = item.component;
141
141
  const Items = items.map((it) => it.component);
142
142
 
143
- const svg = renderSVG(
144
- <Structure
145
- data={data}
146
- Title={Title}
147
- Item={Item}
148
- Items={Items}
149
- options={parsedOptions}
150
- {...structureProps}
151
- />,
152
- );
153
-
154
- const template = parseSVG(svg);
155
- if (!template) {
156
- throw new Error('Failed to parse SVG template');
143
+ // Apply theme font-family before measurement so measureText uses the correct font
144
+ const themeFontFamily = themeConfig?.base?.text?.['font-family'];
145
+ const previousDefaultFont = DEFAULT_FONT;
146
+ if (themeFontFamily) setDefaultFont(themeFontFamily);
147
+
148
+ try {
149
+ const svg = renderSVG(
150
+ <Structure
151
+ data={data}
152
+ Title={Title}
153
+ Item={Item}
154
+ Items={Items}
155
+ options={parsedOptions}
156
+ {...structureProps}
157
+ />,
158
+ );
159
+
160
+ const template = parseSVG(svg);
161
+ if (!template) {
162
+ throw new Error('Failed to parse SVG template');
163
+ }
164
+ return template;
165
+ } finally {
166
+ // Restore previous default font
167
+ if (themeFontFamily) setDefaultFont(previousDefaultFont);
157
168
  }
158
- return template;
159
169
  }
160
170
 
161
171
  getTypes() {