@widgetic/editor 4.0.9 → 4.0.11

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 (55) hide show
  1. package/README.md +118 -0
  2. package/dist/components/WidgetPreview.svelte +65 -27
  3. package/dist/components/WidgetPreview.svelte.d.ts +8 -0
  4. package/dist/components/editors/CompositionEditorApp.svelte +508 -0
  5. package/dist/components/editors/CompositionEditorApp.svelte.d.ts +75 -0
  6. package/dist/components/editors/PropsEditor.svelte +193 -792
  7. package/dist/components/editors/PropsEditor.svelte.d.ts +7 -0
  8. package/dist/components/ic/BrowserInputController.svelte +21 -2
  9. package/dist/components/ic/ColorPickerInputController.svelte +24 -6
  10. package/dist/components/ic/DateFieldInputController.svelte +7 -3
  11. package/dist/components/ic/DatePickerInputController.svelte +10 -4
  12. package/dist/components/ic/DropdownInputController.svelte +11 -4
  13. package/dist/components/ic/FontInputController.svelte +68 -44
  14. package/dist/components/ic/InputControllerHeader.svelte +11 -8
  15. package/dist/components/ic/NumberInputController.svelte +4 -2
  16. package/dist/components/ic/PositionInputController.svelte +14 -11
  17. package/dist/components/ic/RangeCalendarInputController.svelte +10 -4
  18. package/dist/components/ic/RoundedCornersInputController.svelte +17 -9
  19. package/dist/components/ic/TextAreaInputController.svelte +20 -7
  20. package/dist/components/ic/TextAreaPreviewInputController.svelte +19 -69
  21. package/dist/components/ic/TimeInputController.svelte +9 -3
  22. package/dist/components/ic/VectorInputController.svelte +80 -54
  23. package/dist/components/ic/shared/ItemGrid.svelte +5 -1
  24. package/dist/composition/compositionApi.d.ts +95 -0
  25. package/dist/composition/compositionApi.js +289 -0
  26. package/dist/composition/compositionEditorController.d.ts +254 -0
  27. package/dist/composition/compositionEditorController.js +734 -0
  28. package/dist/composition/compositionHelpers.d.ts +72 -0
  29. package/dist/composition/compositionHelpers.js +403 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +4 -0
  32. package/dist/mediaLibraryContext.d.ts +10 -0
  33. package/dist/mediaLibraryContext.js +30 -0
  34. package/dist/utils/FontManager.d.ts +21 -5
  35. package/dist/utils/FontManager.js +62 -67
  36. package/dist/utils/VectorManager.d.ts +0 -1
  37. package/dist/utils/VectorManager.js +10 -12
  38. package/dist/utils/editorStorage.d.ts +23 -0
  39. package/dist/utils/editorStorage.js +95 -0
  40. package/dist/utils/fontUtils.js +5 -3
  41. package/dist/utils/htmlSafety.d.ts +21 -0
  42. package/dist/utils/htmlSafety.js +38 -0
  43. package/dist/utils/itemReorder.d.ts +90 -0
  44. package/dist/utils/itemReorder.js +354 -0
  45. package/dist/utils/recentColors.js +4 -2
  46. package/dist/utils/richTextPreview.d.ts +38 -0
  47. package/dist/utils/richTextPreview.js +207 -0
  48. package/dist/utils/scrollActivity.d.ts +14 -0
  49. package/dist/utils/scrollActivity.js +63 -0
  50. package/dist/utils/shadowDom.d.ts +67 -0
  51. package/dist/utils/shadowDom.js +211 -0
  52. package/dist/utils/vectorUtils.js +5 -3
  53. package/dist/utils/visibilityCondition.d.ts +40 -0
  54. package/dist/utils/visibilityCondition.js +535 -0
  55. package/package.json +6 -5
@@ -35,6 +35,9 @@
35
35
 
36
36
  // Component config
37
37
  import type { InputControllerConfig } from './index';
38
+
39
+ // The cursor of a drag is shown on the module's element, not on the body of the customer's page
40
+ import { lockPointerStyle } from '../../utils/shadowDom';
38
41
 
39
42
  /**
40
43
  * Core state and configuration props
@@ -58,6 +61,8 @@
58
61
  let joystickHandle: HTMLDivElement;
59
62
  let isDragging = false;
60
63
  let isHovering = false;
64
+ // Puts back the cursor the page had before the drag (does nothing when no drag is going on)
65
+ let releasePointerStyle: () => void = () => {};
61
66
 
62
67
  /**
63
68
  * Reactive position state
@@ -125,8 +130,9 @@
125
130
  updatePosition(event.clientX, event.clientY);
126
131
  document.addEventListener('mousemove', handleMouseMove);
127
132
  document.addEventListener('mouseup', handleMouseUp);
128
- document.body.style.cursor = 'grabbing';
129
- document.body.classList.add('position-dragging');
133
+ // A second press before the first one was let go must not leave the first lock behind
134
+ releasePointerStyle();
135
+ releasePointerStyle = lockPointerStyle(joystickContainer, 'grabbing');
130
136
  }
131
137
 
132
138
  function handleMouseMove(event: MouseEvent) {
@@ -139,9 +145,7 @@
139
145
  onDragStateChange(false);
140
146
  document.removeEventListener('mousemove', handleMouseMove);
141
147
  document.removeEventListener('mouseup', handleMouseUp);
142
- document.body.style.cursor = '';
143
- document.body.classList.remove('position-dragging');
144
- document.body.style.userSelect = '';
148
+ releasePointerStyle();
145
149
  }
146
150
 
147
151
  /**
@@ -177,13 +181,12 @@
177
181
  });
178
182
 
179
183
  onDestroy(() => {
180
- document.body.classList.remove('position-dragging');
181
- document.body.style.cursor = '';
182
- document.body.style.userSelect = '';
184
+ // Only undoes what this drag did (it used to clear the cursor and the text selection of the whole page)
185
+ releasePointerStyle();
183
186
  });
184
187
  </script>
185
188
 
186
- <div class="position-input-controller widgetic-colors">
189
+ <div class="position-input-controller widgetic-colors" class:position-input-controller-dragging={isDragging}>
187
190
  <!-- Joystick UI -->
188
191
  <!-- svelte-ignore a11y_no_static_element_interactions -->
189
192
  <div
@@ -228,8 +231,8 @@
228
231
  </div>
229
232
 
230
233
  <style>
231
- /* Make the selector more specific by targeting only elements within position-input-controller */
232
- :global(body.position-dragging .position-input-controller *) {
234
+ /* While dragging, everything inside the controller shows the grabbing cursor (this used to be a class on the page's body) */
235
+ .position-input-controller-dragging :global(*) {
233
236
  cursor: grabbing !important;
234
237
  user-select: none !important;
235
238
  -webkit-user-select: none !important;
@@ -46,6 +46,7 @@
46
46
  <script lang="ts">
47
47
  // Svelte imports
48
48
  import { onMount, onDestroy } from 'svelte';
49
+ import { listenForScroll } from '../../utils/shadowDom';
49
50
 
50
51
  // Shadcn UI Components
51
52
  import { Button, RangeCalendar } from '@widgetic/design-system/components';
@@ -177,19 +178,24 @@
177
178
  closeDropdown();
178
179
  }
179
180
 
180
- // Setup scroll event listener on mount
181
+ // The control's own element, which says which tree (the page or a module's shadow root) to listen for scrolling in
182
+ let controllerElement: HTMLDivElement | undefined;
183
+ let stopListeningForScroll: () => void = () => {};
184
+
185
+ // Setup scroll event listener on mount (the page and, inside a module, the module's own panels: a scroll never
186
+ // leaves a shadow root, so a listener on the window alone does not hear about those)
181
187
  onMount(() => {
182
- window.addEventListener('scroll', handleScroll, true);
188
+ stopListeningForScroll = listenForScroll(controllerElement, handleScroll);
183
189
  });
184
190
 
185
191
  // Cleanup scroll event listener on destroy
186
192
  onDestroy(() => {
187
- window.removeEventListener('scroll', handleScroll, true);
193
+ stopListeningForScroll();
188
194
  });
189
195
  </script>
190
196
 
191
197
  <!-- Range Calendar Container -->
192
- <div class="flex flex-col gap-2 widgetic-colors">
198
+ <div bind:this={controllerElement} class="flex flex-col gap-2 widgetic-colors">
193
199
  <div class="flex items-center w-full relative">
194
200
  <div class="flex-1 min-w-0">
195
201
  <InputControllerHeader {label} helpText={helpText} />
@@ -28,6 +28,7 @@
28
28
  import Lock from '../icons/locked.svelte';
29
29
  import Unlock from '../icons/unlocked.svelte';
30
30
  import type { InputControllerConfig } from './index';
31
+ import { activeElementIn } from '../../utils/shadowDom';
31
32
 
32
33
  // Constants
33
34
  const MIN_RADIUS = 0;
@@ -63,19 +64,22 @@
63
64
 
64
65
  if (isLocked) {
65
66
  // When locked, update all corners to the same value
66
- onChange({
67
+ value = {
67
68
  topLeft: newValue,
68
69
  topRight: newValue,
69
70
  bottomRight: newValue,
70
71
  bottomLeft: newValue
71
- });
72
+ };
72
73
  } else {
73
74
  // When unlocked, update only the specified corner
74
- onChange({
75
+ value = {
75
76
  ...value,
76
77
  [corner]: newValue
77
- });
78
+ };
78
79
  }
80
+ // The fields show the change at once, in every corner it reached: the parent hands the value back only when it
81
+ // builds this control again, and until then the corners that were changed together kept showing the old number
82
+ onChange(value);
79
83
  }
80
84
 
81
85
  // Handler for input changes
@@ -167,9 +171,12 @@
167
171
 
168
172
  // Reset focused corner if we're not focusing another input immediately
169
173
  setTimeout(() => {
170
- if (!document.activeElement ||
171
- !(document.activeElement instanceof HTMLInputElement) ||
172
- !Object.values(inputRefs).includes(document.activeElement)) {
174
+ // The focused element of the tree this control is in: inside a module `document.activeElement` is the module's
175
+ // host element, which is never one of the corners
176
+ const focusedElement = activeElementIn(input);
177
+ if (!focusedElement ||
178
+ !(focusedElement instanceof HTMLInputElement) ||
179
+ !Object.values(inputRefs).includes(focusedElement)) {
173
180
  focusedCorner = null;
174
181
  }
175
182
  }, 0);
@@ -182,12 +189,13 @@
182
189
  // If locking, normalize all values to the first focused corner or top-left
183
190
  if (isLocked && focusedCorner) {
184
191
  const referenceValue = value[focusedCorner];
185
- onChange({
192
+ value = {
186
193
  topLeft: referenceValue,
187
194
  topRight: referenceValue,
188
195
  bottomRight: referenceValue,
189
196
  bottomLeft: referenceValue
190
- });
197
+ };
198
+ onChange(value);
191
199
  }
192
200
  }
193
201
 
@@ -71,6 +71,9 @@
71
71
  // Add local ColorPickerInputController import
72
72
  import ColorPickerInputController from './ColorPickerInputController.svelte';
73
73
 
74
+ // Looking things up and listening for scrolling in the tree the editor is in (a module has a shadow root)
75
+ import { isEventInside, listenForScroll, queryAllInTreeOf, queryInTreeOf } from '../../utils/shadowDom';
76
+
74
77
  // Add this constant for the decoration type
75
78
  const TRANSITION_DURATION = 250;
76
79
 
@@ -533,7 +536,9 @@
533
536
  });
534
537
 
535
538
  updateActiveFormats();
539
+ // Choosing from either menu (text style or list style) closes it: the list style menu used to stay open.
536
540
  textStyleDropdownOpen = false;
541
+ listStyleDropdownOpen = false;
537
542
  } catch (error) {
538
543
  console.error('Error changing text style:', error);
539
544
  }
@@ -843,8 +848,11 @@
843
848
  if (isDestroyed) return;
844
849
  const handleClickOutside = (event: MouseEvent) => {
845
850
  if (isDestroyed) return;
846
- const colorSelectModal = document.querySelector('[data-radix-popper-content-wrapper]');
847
- const isClickingColorSelect = colorSelectModal?.contains(event.target as Node);
851
+ // Looked for in the tree this editor is in (inside a module, `document` has none of the editor's elements and may
852
+ // have a popover of the customer's page), and checked with the event's path (the target the document sees is the
853
+ // module's host)
854
+ const colorSelectModals = queryAllInTreeOf(toolbarRef, '[data-radix-popper-content-wrapper]');
855
+ const isClickingColorSelect = colorSelectModals.some((colorSelectModal) => isEventInside(event, colorSelectModal));
848
856
 
849
857
  // Don't do anything if clicking within the color select modal
850
858
  if (isClickingColorSelect) {
@@ -941,9 +949,11 @@
941
949
  function handleEditorBlur(relatedTarget: Node | null) {
942
950
  if (!editorInstance) return;
943
951
 
944
- const colorSelectModal = document.querySelector('[data-radix-popper-content-wrapper]');
945
- const popoverContent = document.querySelector('[role="dialog"]');
946
- const commandMenu = document.querySelector('[cmdk-root]');
952
+ // Looked for in the tree the editor is in: `document` does not see inside a module's shadow root, and may have a
953
+ // dialog of the customer's page
954
+ const colorSelectModal = queryInTreeOf(toolbarRef, '[data-radix-popper-content-wrapper]');
955
+ const popoverContent = queryInTreeOf(toolbarRef, '[role="dialog"]');
956
+ const commandMenu = queryInTreeOf(toolbarRef, '[cmdk-root]');
947
957
 
948
958
  const isClickingInside = toolbarRef?.contains(relatedTarget) ||
949
959
  editorInstance.view.dom.contains(relatedTarget as Node) ||
@@ -1030,12 +1040,15 @@
1030
1040
  }
1031
1041
 
1032
1042
  // Add to lifecycle management
1043
+ // (listens to the page and, inside a module, to the module's own panels too: a scroll never leaves a shadow root)
1044
+ let stopListeningForScroll: () => void = () => {};
1045
+
1033
1046
  onMount(() => {
1034
- window.addEventListener('scroll', handleScroll, true);
1047
+ stopListeningForScroll = listenForScroll(toolbarRef, handleScroll);
1035
1048
  });
1036
1049
 
1037
1050
  onDestroy(() => {
1038
- window.removeEventListener('scroll', handleScroll, true);
1051
+ stopListeningForScroll();
1039
1052
  });
1040
1053
 
1041
1054
  // Add cleanup function
@@ -19,6 +19,8 @@
19
19
  import { onMount, onDestroy } from 'svelte';
20
20
 
21
21
  import { TextAreaInputController } from './index';
22
+ import { createPreviewHtml } from '../../utils/richTextPreview';
23
+ import { escapeHtml } from '../../utils/htmlSafety';
22
24
 
23
25
  /**
24
26
  * Component props
@@ -42,7 +44,7 @@
42
44
  $: isShowingPlaceholder = !value && placeholder;
43
45
 
44
46
  // Create a preview version of the text that preserves formatting
45
- $: previewText = value ? createPreviewText(value) : placeholder;
47
+ $: previewText = value ? createPreviewText(value) : escapeHtml(placeholder);
46
48
 
47
49
  // Add this to track the current color
48
50
  let currentColor = '#1a1a1a'; // Default color
@@ -53,6 +55,10 @@
53
55
  // Add this to store the background rect ref
54
56
  let backgroundRectRef: HTMLDivElement;
55
57
 
58
+ // The box the text editor is shown in. The editor is found through it: a search of the whole `document` finds nothing
59
+ // inside a module's shadow root, and finds the customer's own text editor (and focuses it) when the page has one.
60
+ let portalContentRef: HTMLDivElement | undefined;
61
+
56
62
  // Add these near the top with other state declarations
57
63
  let isClosing = false;
58
64
 
@@ -60,73 +66,16 @@
60
66
  * Methods
61
67
  */
62
68
  function createPreviewText(text: string) {
63
- if (!text) return '';
64
-
65
- let html = text;
66
- if (!html.includes('<') && html.includes('&lt;')) {
67
- const decoded = document.createElement('textarea');
68
- decoded.innerHTML = html;
69
- html = decoded.value;
70
- }
71
-
72
- const parser = new DOMParser();
73
- const doc = parser.parseFromString(html, 'text/html');
74
-
75
- function processNode(node: Node): string {
76
- if (node.nodeType === Node.TEXT_NODE) {
77
- return node.textContent || '';
78
- }
79
-
80
- if (node.nodeType === Node.ELEMENT_NODE) {
81
- const element = node as Element;
82
- let tag = element.tagName.toLowerCase();
83
- const attributes: Record<string, string> = {};
84
- const children = Array.from(node.childNodes)
85
- .map(child => processNode(child))
86
- .join('');
87
-
88
- // Flatten block wrappers so marks render inside the compact clamp box
89
- if (tag === 'p' || tag === 'div' || tag === 'br') {
90
- return tag === 'br' ? ' ' : `${children} `;
91
- }
92
-
93
- const style = element.getAttribute('style');
94
- if (style) {
95
- const colorMatch = style.match(/color:\s*([^;]+)/);
96
- if (colorMatch) {
97
- attributes.style = `--color: ${colorMatch[1]}; ${style}`;
98
- } else {
99
- attributes.style = style;
100
- }
101
- }
102
-
103
- const textStyle = element.getAttribute('data-text-style');
104
- if (textStyle) {
105
- attributes['data-text-style'] = textStyle;
106
- attributes.class = (attributes.class || '') + ' preserve-color';
107
- }
108
-
109
- if (tag.match(/^h[1-4]$/)) {
110
- tag = 'span';
111
- attributes.class = (attributes.class || '') + ' tiptap-heading';
112
- }
113
-
114
- let startTag = `<${tag}`;
115
- for (const [key, attrValue] of Object.entries(attributes)) {
116
- startTag += ` ${key}="${attrValue.trim()}"`;
117
- }
118
- startTag += '>';
119
-
120
- return `${startTag}${children}</${tag}>`;
121
- }
122
-
123
- return '';
69
+ // The text is rich text somebody typed or pasted, and this editor can run inside a customer's page: the preview is
70
+ // not a copy of that HTML. It is built by a renderer that escapes every text and keeps only the tags and styles on
71
+ // its lists (see richTextPreview.ts). Block wrappers (p, div, br) are flattened there, so marks render inside the
72
+ // compact clamp box.
73
+ try {
74
+ return createPreviewHtml(text);
75
+ } catch (error) {
76
+ console.error('[TextAreaPreview] Could not build the preview of the text:', error);
77
+ return escapeHtml(text);
124
78
  }
125
-
126
- return Array.from(doc.body.childNodes)
127
- .map(node => processNode(node))
128
- .join('')
129
- .trim();
130
79
  }
131
80
 
132
81
  function handleFullEditorChange(html: string) {
@@ -141,7 +90,7 @@
141
90
 
142
91
  $: if (isShowingTextArea) {
143
92
  setTimeout(() => {
144
- const editor = document.querySelector('.ProseMirror');
93
+ const editor = portalContentRef?.querySelector('.ProseMirror');
145
94
  if (editor instanceof HTMLElement) {
146
95
  editor.focus();
147
96
  }
@@ -213,7 +162,7 @@
213
162
  isClosing = true;
214
163
 
215
164
  // Add animation end handler
216
- const content = document.querySelector('.text-area-portal-content') as HTMLDivElement;
165
+ const content = portalContentRef;
217
166
  const overlay = content?.previousElementSibling as HTMLDivElement;
218
167
 
219
168
  const handleAnimationEnd = () => {
@@ -280,6 +229,7 @@
280
229
  ></div>
281
230
  <!-- svelte-ignore a11y_no_static_element_interactions -->
282
231
  <div
232
+ bind:this={portalContentRef}
283
233
  class="text-area-portal-content absolute mt-4 mx-6 mb-6 inset-0 {isClosing ? 'scale-out-animation' : 'scale-in-animation'}"
284
234
  style="z-index: 51; transform: translateZ(0); backface-visibility: hidden;"
285
235
  on:mouseenter={handlePortalMouseEnter}
@@ -29,6 +29,7 @@
29
29
  import { Portal } from '@widgetic/design-system/components';
30
30
  import InputControllerHeader from './InputControllerHeader.svelte';
31
31
  import type { InputControllerConfig } from './index';
32
+ import { isEventInside, listenForScroll } from '../../utils/shadowDom';
32
33
 
33
34
  export let label: TimeInputControllerProps['label'];
34
35
  export let value: TimeInputControllerProps['value'] = "00:00";
@@ -256,7 +257,9 @@
256
257
  }
257
258
 
258
259
  function handleClickOutside(event: MouseEvent) {
259
- if (dropdownContainer && !dropdownContainer.contains(event.target as Node)) {
260
+ // Seen from the document, a click inside a shadow root has the module's host as its target, so the click's path
261
+ // is what says whether it was inside the dropdown
262
+ if (dropdownContainer && !isEventInside(event, dropdownContainer)) {
260
263
  closeDropdown();
261
264
  }
262
265
  }
@@ -290,11 +293,14 @@
290
293
  }
291
294
  }
292
295
 
296
+ // Stops the scroll listener (it listens to the page and, in a module, to the module's own panels)
297
+ let stopListeningForScroll: () => void = () => {};
298
+
293
299
  // Update onMount to add scroll listener with capture phase
294
300
  onMount(() => {
295
301
  parseTimeFromValue(value);
296
302
  updateValue();
297
- window.addEventListener('scroll', handleScroll, { capture: true });
303
+ stopListeningForScroll = listenForScroll(clockContainer, handleScroll);
298
304
  });
299
305
 
300
306
  // Update onDestroy to remove scroll listener
@@ -302,7 +308,7 @@
302
308
  document.removeEventListener('mousemove', handleMouseMove);
303
309
  document.removeEventListener('mouseup', handleMouseUp);
304
310
  document.removeEventListener('click', handleClickOutside);
305
- window.removeEventListener('scroll', handleScroll, true);
311
+ stopListeningForScroll();
306
312
  });
307
313
 
308
314
  // Watch for external value changes