@nordcraft/runtime 1.0.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 (168) hide show
  1. package/README.md +5 -0
  2. package/dist/api/createAPI.d.ts +20 -0
  3. package/dist/api/createAPI.js +319 -0
  4. package/dist/api/createAPI.js.map +1 -0
  5. package/dist/api/createAPIv2.d.ts +7 -0
  6. package/dist/api/createAPIv2.js +686 -0
  7. package/dist/api/createAPIv2.js.map +1 -0
  8. package/dist/components/createComponent.d.ts +13 -0
  9. package/dist/components/createComponent.js +216 -0
  10. package/dist/components/createComponent.js.map +1 -0
  11. package/dist/components/createElement.d.ts +3 -0
  12. package/dist/components/createElement.js +208 -0
  13. package/dist/components/createElement.js.map +1 -0
  14. package/dist/components/createNode.d.ts +22 -0
  15. package/dist/components/createNode.js +272 -0
  16. package/dist/components/createNode.js.map +1 -0
  17. package/dist/components/createSlot.d.ts +3 -0
  18. package/dist/components/createSlot.js +49 -0
  19. package/dist/components/createSlot.js.map +1 -0
  20. package/dist/components/createText.d.ts +23 -0
  21. package/dist/components/createText.js +68 -0
  22. package/dist/components/createText.js.map +1 -0
  23. package/dist/components/createText.test.d.ts +1 -0
  24. package/dist/components/createText.test.js +113 -0
  25. package/dist/components/createText.test.js.map +1 -0
  26. package/dist/components/renderComponent.d.ts +34 -0
  27. package/dist/components/renderComponent.js +66 -0
  28. package/dist/components/renderComponent.js.map +1 -0
  29. package/dist/context/isContextProvider.d.ts +2 -0
  30. package/dist/context/isContextProvider.js +5 -0
  31. package/dist/context/isContextProvider.js.map +1 -0
  32. package/dist/context/subscribeToContext.d.ts +4 -0
  33. package/dist/context/subscribeToContext.js +93 -0
  34. package/dist/context/subscribeToContext.js.map +1 -0
  35. package/dist/custom-components/components.d.ts +1 -0
  36. package/dist/custom-components/components.js +2 -0
  37. package/dist/custom-components/components.js.map +1 -0
  38. package/dist/custom-components/toddle-portal.d.ts +6 -0
  39. package/dist/custom-components/toddle-portal.js +20 -0
  40. package/dist/custom-components/toddle-portal.js.map +1 -0
  41. package/dist/custom-element/ToddleComponent.d.ts +37 -0
  42. package/dist/custom-element/ToddleComponent.js +244 -0
  43. package/dist/custom-element/ToddleComponent.js.map +1 -0
  44. package/dist/custom-element/defineComponents.d.ts +26 -0
  45. package/dist/custom-element/defineComponents.js +42 -0
  46. package/dist/custom-element/defineComponents.js.map +1 -0
  47. package/dist/custom-element.main.d.ts +3 -0
  48. package/dist/custom-element.main.esm.js +266 -0
  49. package/dist/custom-element.main.esm.js.map +7 -0
  50. package/dist/custom-element.main.js +14 -0
  51. package/dist/custom-element.main.js.map +1 -0
  52. package/dist/debug/logState.d.ts +4 -0
  53. package/dist/debug/logState.js +19 -0
  54. package/dist/debug/logState.js.map +1 -0
  55. package/dist/editor/drag-drop/dragEnded.d.ts +2 -0
  56. package/dist/editor/drag-drop/dragEnded.js +56 -0
  57. package/dist/editor/drag-drop/dragEnded.js.map +1 -0
  58. package/dist/editor/drag-drop/dragMove.d.ts +3 -0
  59. package/dist/editor/drag-drop/dragMove.js +74 -0
  60. package/dist/editor/drag-drop/dragMove.js.map +1 -0
  61. package/dist/editor/drag-drop/dragReorder.d.ts +3 -0
  62. package/dist/editor/drag-drop/dragReorder.js +92 -0
  63. package/dist/editor/drag-drop/dragReorder.js.map +1 -0
  64. package/dist/editor/drag-drop/dragStarted.d.ts +9 -0
  65. package/dist/editor/drag-drop/dragStarted.js +100 -0
  66. package/dist/editor/drag-drop/dragStarted.js.map +1 -0
  67. package/dist/editor/drag-drop/dropHighlight.d.ts +16 -0
  68. package/dist/editor/drag-drop/dropHighlight.js +50 -0
  69. package/dist/editor/drag-drop/dropHighlight.js.map +1 -0
  70. package/dist/editor/drag-drop/getInsertAreas.d.ts +20 -0
  71. package/dist/editor/drag-drop/getInsertAreas.js +220 -0
  72. package/dist/editor/drag-drop/getInsertAreas.js.map +1 -0
  73. package/dist/editor-preview.main.d.ts +19 -0
  74. package/dist/editor-preview.main.js +1303 -0
  75. package/dist/editor-preview.main.js.map +1 -0
  76. package/dist/events/handleAction.d.ts +3 -0
  77. package/dist/events/handleAction.js +307 -0
  78. package/dist/events/handleAction.js.map +1 -0
  79. package/dist/page.main.d.ts +7 -0
  80. package/dist/page.main.esm.js +8 -0
  81. package/dist/page.main.esm.js.map +7 -0
  82. package/dist/page.main.js +395 -0
  83. package/dist/page.main.js.map +1 -0
  84. package/dist/signal/signal.d.ts +19 -0
  85. package/dist/signal/signal.js +65 -0
  86. package/dist/signal/signal.js.map +1 -0
  87. package/dist/styles/style.d.ts +4 -0
  88. package/dist/styles/style.js +196 -0
  89. package/dist/styles/style.js.map +1 -0
  90. package/dist/utils/BatchQueue.d.ts +10 -0
  91. package/dist/utils/BatchQueue.js +25 -0
  92. package/dist/utils/BatchQueue.js.map +1 -0
  93. package/dist/utils/createFormulaCache.d.ts +3 -0
  94. package/dist/utils/createFormulaCache.js +81 -0
  95. package/dist/utils/createFormulaCache.js.map +1 -0
  96. package/dist/utils/findNearestLine.d.ts +13 -0
  97. package/dist/utils/findNearestLine.js +74 -0
  98. package/dist/utils/findNearestLine.js.map +1 -0
  99. package/dist/utils/findNearestLine.test.d.ts +1 -0
  100. package/dist/utils/findNearestLine.test.js +59 -0
  101. package/dist/utils/findNearestLine.test.js.map +1 -0
  102. package/dist/utils/getDragData.d.ts +1 -0
  103. package/dist/utils/getDragData.js +10 -0
  104. package/dist/utils/getDragData.js.map +1 -0
  105. package/dist/utils/getElementTagName.d.ts +3 -0
  106. package/dist/utils/getElementTagName.js +7 -0
  107. package/dist/utils/getElementTagName.js.map +1 -0
  108. package/dist/utils/nodes.d.ts +21 -0
  109. package/dist/utils/nodes.js +89 -0
  110. package/dist/utils/nodes.js.map +1 -0
  111. package/dist/utils/omitStyle.d.ts +2 -0
  112. package/dist/utils/omitStyle.js +13 -0
  113. package/dist/utils/omitStyle.js.map +1 -0
  114. package/dist/utils/rectHasPoint.d.ts +2 -0
  115. package/dist/utils/rectHasPoint.js +4 -0
  116. package/dist/utils/rectHasPoint.js.map +1 -0
  117. package/dist/utils/setAttribute.d.ts +4 -0
  118. package/dist/utils/setAttribute.js +57 -0
  119. package/dist/utils/setAttribute.js.map +1 -0
  120. package/dist/utils/tryStartViewTransition.d.ts +5 -0
  121. package/dist/utils/tryStartViewTransition.js +14 -0
  122. package/dist/utils/tryStartViewTransition.js.map +1 -0
  123. package/dist/utils/url.d.ts +2 -0
  124. package/dist/utils/url.js +36 -0
  125. package/dist/utils/url.js.map +1 -0
  126. package/package.json +25 -0
  127. package/src/api/createAPI.ts +375 -0
  128. package/src/api/createAPIv2.ts +931 -0
  129. package/src/components/createComponent.ts +280 -0
  130. package/src/components/createElement.ts +240 -0
  131. package/src/components/createNode.ts +381 -0
  132. package/src/components/createSlot.ts +61 -0
  133. package/src/components/createText.test.ts +117 -0
  134. package/src/components/createText.ts +104 -0
  135. package/src/components/renderComponent.ts +145 -0
  136. package/src/context/isContextProvider.ts +12 -0
  137. package/src/context/subscribeToContext.ts +135 -0
  138. package/src/custom-components/components.ts +1 -0
  139. package/src/custom-components/toddle-portal.ts +19 -0
  140. package/src/custom-element/ToddleComponent.ts +315 -0
  141. package/src/custom-element/defineComponents.ts +65 -0
  142. package/src/custom-element.main.ts +24 -0
  143. package/src/debug/logState.ts +30 -0
  144. package/src/editor/drag-drop/dragEnded.ts +75 -0
  145. package/src/editor/drag-drop/dragMove.ts +95 -0
  146. package/src/editor/drag-drop/dragReorder.ts +137 -0
  147. package/src/editor/drag-drop/dragStarted.ts +145 -0
  148. package/src/editor/drag-drop/dropHighlight.ts +82 -0
  149. package/src/editor/drag-drop/getInsertAreas.ts +235 -0
  150. package/src/editor/types.d.ts +36 -0
  151. package/src/editor-preview.main.ts +1782 -0
  152. package/src/events/handleAction.ts +387 -0
  153. package/src/page.main.ts +489 -0
  154. package/src/signal/signal.ts +74 -0
  155. package/src/styles/style.ts +254 -0
  156. package/src/types.d.ts +93 -0
  157. package/src/utils/BatchQueue.ts +24 -0
  158. package/src/utils/createFormulaCache.ts +96 -0
  159. package/src/utils/findNearestLine.test.ts +65 -0
  160. package/src/utils/findNearestLine.ts +92 -0
  161. package/src/utils/getDragData.ts +11 -0
  162. package/src/utils/getElementTagName.ts +14 -0
  163. package/src/utils/nodes.ts +125 -0
  164. package/src/utils/omitStyle.ts +19 -0
  165. package/src/utils/rectHasPoint.ts +5 -0
  166. package/src/utils/setAttribute.ts +56 -0
  167. package/src/utils/tryStartViewTransition.ts +32 -0
  168. package/src/utils/url.ts +45 -0
@@ -0,0 +1,1303 @@
1
+ /* eslint-disable no-console */
2
+ /* eslint-disable @typescript-eslint/prefer-optional-chain */
3
+ /* eslint-disable no-case-declarations */
4
+ /* eslint-disable no-fallthrough */
5
+ import { isLegacyApi } from '@nordcraft/core/dist/api/api';
6
+ import { isPageComponent } from '@nordcraft/core/dist/component/isPageComponent';
7
+ import { applyFormula } from '@nordcraft/core/dist/formula/formula';
8
+ import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils';
9
+ import { getClassName } from '@nordcraft/core/dist/styling/className';
10
+ import { getThemeCss } from '@nordcraft/core/dist/styling/theme';
11
+ import { theme } from '@nordcraft/core/dist/styling/theme.const';
12
+ import { mapObject, omitKeys } from '@nordcraft/core/dist/utils/collections';
13
+ import * as libActions from '@nordcraft/std-lib/dist/actions';
14
+ import * as libFormulas from '@nordcraft/std-lib/dist/formulas';
15
+ import fastDeepEqual from 'fast-deep-equal';
16
+ import { createLegacyAPI } from './api/createAPI';
17
+ import { createAPI } from './api/createAPIv2';
18
+ import { createNode } from './components/createNode';
19
+ import { isContextProvider } from './context/isContextProvider';
20
+ import { dragEnded } from './editor/drag-drop/dragEnded';
21
+ import { dragMove } from './editor/drag-drop/dragMove';
22
+ import { dragReorder } from './editor/drag-drop/dragReorder';
23
+ import { dragStarted } from './editor/drag-drop/dragStarted';
24
+ import { handleAction } from './events/handleAction';
25
+ import { signal } from './signal/signal';
26
+ import { insertStyles, styleToCss } from './styles/style';
27
+ import { createFormulaCache } from './utils/createFormulaCache';
28
+ import { getNodeAndAncestors, isNodeOrAncestorConditional } from './utils/nodes';
29
+ import { omitSubnodeStyleForComponent } from './utils/omitStyle';
30
+ import { rectHasPoint } from './utils/rectHasPoint';
31
+ /**
32
+ * Styles required for rendering the same exact text again somewhere else (on a overlay rect in the editor)
33
+ */
34
+ var TextNodeComputedStyles;
35
+ (function (TextNodeComputedStyles) {
36
+ // Caret color is important as it is the only visible part of the text node (when text is not highlighted)
37
+ TextNodeComputedStyles["CARET_COLOR"] = "caret-color";
38
+ TextNodeComputedStyles["FONT_FAMILY"] = "font-family";
39
+ TextNodeComputedStyles["FONT_SIZE"] = "font-size";
40
+ TextNodeComputedStyles["FONT_WEIGHT"] = "font-weight";
41
+ TextNodeComputedStyles["FONT_STYLE"] = "font-style";
42
+ TextNodeComputedStyles["FONT_VARIANT"] = "font-variant";
43
+ TextNodeComputedStyles["FONT_STRETCH"] = "font-stretch";
44
+ TextNodeComputedStyles["LINE_HEIGHT"] = "line-height";
45
+ TextNodeComputedStyles["TEXT_ALIGN"] = "text-align";
46
+ TextNodeComputedStyles["TEXT_TRANSFORM"] = "text-transform";
47
+ TextNodeComputedStyles["LETTER_SPACING"] = "letter-spacing";
48
+ TextNodeComputedStyles["WHITE_SPACE"] = "white-space";
49
+ TextNodeComputedStyles["WORD_SPACING"] = "word-spacing";
50
+ TextNodeComputedStyles["TEXT_INDENT"] = "text-indent";
51
+ TextNodeComputedStyles["TEXT_OVERFLOW"] = "text-overflow";
52
+ TextNodeComputedStyles["TEXT_RENDERING"] = "text-rendering";
53
+ TextNodeComputedStyles["WORD_BREAK"] = "word-break";
54
+ TextNodeComputedStyles["WORD_WRAP"] = "word-wrap";
55
+ TextNodeComputedStyles["DIRECTION"] = "direction";
56
+ TextNodeComputedStyles["UNICODE_BIDI"] = "unicode-bidi";
57
+ TextNodeComputedStyles["VERTICAL_ALIGN"] = "vertical-align";
58
+ })(TextNodeComputedStyles || (TextNodeComputedStyles = {}));
59
+ let env;
60
+ export const initGlobalObject = ({ formulas, actions, }) => {
61
+ env = {
62
+ isServer: false,
63
+ branchName: window.__toddle.branch,
64
+ request: undefined,
65
+ runtime: 'preview',
66
+ logErrors: true,
67
+ };
68
+ window.toddle = (() => {
69
+ const legacyActions = {};
70
+ const legacyFormulas = {};
71
+ const argumentInputDataList = {};
72
+ const toddle = {
73
+ isEqual: fastDeepEqual,
74
+ errors: [],
75
+ formulas,
76
+ actions,
77
+ registerAction: (name, handler) => {
78
+ if (legacyActions[name]) {
79
+ console.error('There already exists an action with the name ', name);
80
+ return;
81
+ }
82
+ legacyActions[name] = handler;
83
+ },
84
+ getAction: (name) => legacyActions[name],
85
+ registerFormula: (name, handler, getArgumentInputData) => {
86
+ if (legacyFormulas[name]) {
87
+ console.error('There already exists a formula with the name ', name);
88
+ return;
89
+ }
90
+ legacyFormulas[name] = handler;
91
+ if (getArgumentInputData) {
92
+ argumentInputDataList[name] = getArgumentInputData;
93
+ }
94
+ },
95
+ getFormula: (name) => legacyFormulas[name],
96
+ getCustomAction: (name, packageName) => {
97
+ return (toddle.actions[packageName ?? window.__toddle.project]?.[name] ??
98
+ toddle.actions[window.__toddle.project]?.[name]);
99
+ },
100
+ getCustomFormula: (name, packageName) => {
101
+ return (toddle.formulas[packageName ?? window.__toddle.project]?.[name] ??
102
+ toddle.formulas[window.__toddle.project]?.[name]);
103
+ },
104
+ // eslint-disable-next-line max-params
105
+ getArgumentInputData: (formulaName, args, argIndex, data) => argumentInputDataList[formulaName]?.(args, argIndex, data) || data,
106
+ data: {},
107
+ eventLog: [],
108
+ project: window.__toddle.project,
109
+ branch: window.__toddle.branch,
110
+ commit: window.__toddle.commit,
111
+ components: window.__toddle.components,
112
+ pageState: window.__toddle.pageState,
113
+ locationSignal: signal({
114
+ query: {},
115
+ params: {},
116
+ }),
117
+ env,
118
+ };
119
+ return toddle;
120
+ })();
121
+ // load default formulas and actions
122
+ Object.entries(libFormulas).forEach(([name, module]) => window.toddle.registerFormula('@toddle/' + name, module.default, 'getArgumentInputData' in module
123
+ ? module.getArgumentInputData
124
+ : undefined));
125
+ Object.entries(libActions).forEach(([name, module]) => window.toddle.registerAction('@toddle/' + name, module.default));
126
+ };
127
+ // imported by "/.toddle/preview" (see worker/src/preview.ts)
128
+ export const createRoot = (domNode = document.getElementById('App')) => {
129
+ if (!domNode) {
130
+ throw new Error('Cant find root domNode');
131
+ }
132
+ const isInputTarget = (event) => {
133
+ const target = event.target;
134
+ if (target instanceof HTMLElement) {
135
+ if (target.tagName === 'INPUT' ||
136
+ target.tagName === 'TEXTAREA' ||
137
+ target.tagName === 'SELECT' ||
138
+ target.tagName === 'STYLE-EDITOR') {
139
+ return true;
140
+ }
141
+ if (target.contentEditable?.toLocaleLowerCase() === 'true') {
142
+ return true;
143
+ }
144
+ }
145
+ return false;
146
+ };
147
+ insertTheme(document.head, theme);
148
+ const dataSignal = signal({
149
+ Location: {
150
+ query: {},
151
+ params: {},
152
+ page: '/',
153
+ path: '/',
154
+ hash: '',
155
+ },
156
+ Attributes: {},
157
+ Variables: {},
158
+ });
159
+ let ctxDataSignal;
160
+ let ctx = null;
161
+ let mode = 'design';
162
+ // Signal for overriding conditional elements when they're
163
+ // selected in design mode and for reverting back to normal
164
+ // in test mode
165
+ const showSignal = signal({
166
+ displayedNodes: [],
167
+ testMode: false,
168
+ });
169
+ window.toddle._preview = { showSignal };
170
+ document.body.setAttribute('data-mode', 'design');
171
+ let components = null;
172
+ let packageComponents = null;
173
+ const getAllComponents = () => [
174
+ ...(components ?? []),
175
+ ...(packageComponents ?? []),
176
+ ];
177
+ let component = null;
178
+ let selectedNodeId = null;
179
+ let highlightedNodeId = null;
180
+ let styleVariantSelection = null;
181
+ let routeSignal = null;
182
+ let dragState = null;
183
+ let animationState = null;
184
+ let altKey = false;
185
+ let metaKey = false;
186
+ let previewStyleAnimationFrame = -1;
187
+ /**
188
+ * Modifies all link nodes on a component
189
+ * NOTE: alters in place
190
+ */
191
+ const updateComponentLinks = (component) => {
192
+ // Find all links and add target="_blank" to them
193
+ Object.entries(component.nodes ?? {}).forEach(([_, node]) => {
194
+ if (node.type === 'element' && node.tag === 'a') {
195
+ node.attrs['target'] = valueFormula('_blank');
196
+ }
197
+ });
198
+ return component;
199
+ };
200
+ window.addEventListener('message', (message) => {
201
+ if (!message.isTrusted) {
202
+ console.error('UNTRUSTED MESSAGE');
203
+ }
204
+ switch (message.data?.type) {
205
+ case 'update':
206
+ {
207
+ if (highlightedNodeId) {
208
+ const highlightedNode = getDOMNodeFromNodeId(highlightedNodeId);
209
+ if (highlightedNode) {
210
+ window.parent?.postMessage({
211
+ type: 'highlightRect',
212
+ rect: getRectData(highlightedNode),
213
+ }, '*');
214
+ }
215
+ }
216
+ if (selectedNodeId) {
217
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
218
+ if (selectedNode) {
219
+ window.parent?.postMessage({
220
+ type: 'selectionRect',
221
+ rect: getRectData(selectedNode),
222
+ }, '*');
223
+ }
224
+ }
225
+ }
226
+ break;
227
+ case 'component': {
228
+ if (!message.data.component) {
229
+ return;
230
+ }
231
+ if (message.data.component.name != component?.name) {
232
+ showSignal.cleanSubscribers();
233
+ }
234
+ component = updateComponentLinks(message.data.component);
235
+ if (components && packageComponents && ctx) {
236
+ // Since we're not receiving the current component in
237
+ // "components" updates (see `SetupCanvas` action)
238
+ // we need to manually update the component in components
239
+ const componentIndex = components.findIndex((c) => c.name === component.name);
240
+ if (componentIndex !== -1) {
241
+ components[componentIndex] = component;
242
+ }
243
+ else {
244
+ components.push(component);
245
+ }
246
+ ctx.components = getAllComponents();
247
+ }
248
+ dataSignal.update((data) => {
249
+ const newData = {
250
+ ...data,
251
+ Location: data.Location
252
+ ? {
253
+ ...data.Location,
254
+ path: component?.page ?? '',
255
+ }
256
+ : undefined,
257
+ // Ensure that URL parameters are only available for pages and not components
258
+ 'URL parameters': component?.route
259
+ ? data['URL parameters']
260
+ : undefined,
261
+ };
262
+ return newData;
263
+ });
264
+ update();
265
+ if (highlightedNodeId) {
266
+ const highlightedNode = getDOMNodeFromNodeId(highlightedNodeId);
267
+ if (highlightedNode) {
268
+ window.parent?.postMessage({
269
+ type: 'highlightRect',
270
+ rect: getRectData(highlightedNode),
271
+ }, '*');
272
+ }
273
+ }
274
+ if (selectedNodeId) {
275
+ if (styleVariantSelection) {
276
+ updateSelectedStyleVariant(styleVariantSelection.styleVariantIndex);
277
+ }
278
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
279
+ if (selectedNode) {
280
+ window.parent?.postMessage({
281
+ type: 'selectionRect',
282
+ rect: getRectData(selectedNode),
283
+ }, '*');
284
+ }
285
+ }
286
+ break;
287
+ }
288
+ case 'components': {
289
+ if (Array.isArray(message.data.components)) {
290
+ components = message.data.components.map(updateComponentLinks);
291
+ const allComponents = getAllComponents();
292
+ if (ctx) {
293
+ ctx.components = allComponents;
294
+ }
295
+ updateStyle();
296
+ update();
297
+ }
298
+ break;
299
+ }
300
+ case 'packages': {
301
+ if (message.data.packages) {
302
+ packageComponents = Object.values(message.data.packages ?? {})
303
+ .flatMap((p) => Object.values(p.components).map((c) => ({
304
+ ...c,
305
+ name: `${p.manifest.name}/${c.name}`,
306
+ })))
307
+ .map(updateComponentLinks);
308
+ const allComponents = getAllComponents();
309
+ if (ctx) {
310
+ ctx.components = allComponents;
311
+ }
312
+ updateStyle();
313
+ update();
314
+ }
315
+ break;
316
+ }
317
+ case 'theme': {
318
+ insertTheme(document.head, message.data.theme);
319
+ break;
320
+ }
321
+ case 'mode': {
322
+ mode = message.data.mode;
323
+ document.body.setAttribute('data-mode', message.data.mode);
324
+ updateConditionalElements();
325
+ break;
326
+ }
327
+ case 'attrs': {
328
+ if (message.data.attrs &&
329
+ fastDeepEqual(message.data.attrs, dataSignal.get().Attributes) ===
330
+ false) {
331
+ const attrs = message.data.attrs;
332
+ dataSignal.update((data) => {
333
+ // TODO: We should figure out if "Props" is used anywhere and get rid of it if it's not
334
+ const newData = {
335
+ ...data,
336
+ Location: data.Location && component?.page
337
+ ? {
338
+ ...data.Location,
339
+ query: attrs,
340
+ }
341
+ : data.Location,
342
+ Props: attrs ?? {},
343
+ };
344
+ return newData;
345
+ });
346
+ }
347
+ break;
348
+ }
349
+ case 'selection': {
350
+ if (selectedNodeId !== message.data.selectedNodeId) {
351
+ selectedNodeId = message.data.selectedNodeId ?? null;
352
+ clearSelectedStyleVariant();
353
+ updateConditionalElements();
354
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
355
+ window.parent?.postMessage({
356
+ type: 'selectionRect',
357
+ rect: getRectData(selectedNode),
358
+ }, '*');
359
+ const node = getDOMNodeFromNodeId(selectedNodeId);
360
+ const element = component?.nodes[node?.getAttribute('data-node-id') ?? ''];
361
+ if (node &&
362
+ element &&
363
+ element.type === 'text' &&
364
+ element.value.type === 'value') {
365
+ const computedStyle = window.getComputedStyle(node);
366
+ window.parent?.postMessage({
367
+ type: 'textComputedStyle',
368
+ computedStyle: Object.fromEntries(Object.values(TextNodeComputedStyles).map((style) => [
369
+ style,
370
+ computedStyle.getPropertyValue(style),
371
+ ])),
372
+ }, '*');
373
+ }
374
+ else if (node && node.getAttribute('data-node-type') !== 'text') {
375
+ // Reset computed style on blur
376
+ window.parent?.postMessage({
377
+ type: 'textComputedStyle',
378
+ computedStyle: {},
379
+ }, '*');
380
+ }
381
+ }
382
+ return;
383
+ }
384
+ case 'update_inner_text': {
385
+ const { innerText } = message.data;
386
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
387
+ if (selectedNode &&
388
+ selectedNode.getAttribute('data-node-type') === 'text') {
389
+ ;
390
+ selectedNode.innerText = innerText;
391
+ window.parent?.postMessage({
392
+ type: 'selectionRect',
393
+ rect: getRectData(selectedNode),
394
+ }, '*');
395
+ }
396
+ return;
397
+ }
398
+ case 'highlight': {
399
+ if (highlightedNodeId !== message.data.highlightedNodeId) {
400
+ highlightedNodeId = message.data.highlightedNodeId ?? null;
401
+ const highlightedNode = getDOMNodeFromNodeId(highlightedNodeId);
402
+ window.parent?.postMessage({
403
+ type: 'highlightRect',
404
+ rect: getRectData(highlightedNode),
405
+ }, '*');
406
+ }
407
+ return;
408
+ }
409
+ case 'mousemove':
410
+ if (dragState && !dragState.destroying) {
411
+ const { x, y } = message.data;
412
+ dragState.lastCursorPosition = { x, y };
413
+ const draggingInsideContainer = rectHasPoint(dragState.initialContainer.getBoundingClientRect(), { x, y });
414
+ if (draggingInsideContainer && !metaKey) {
415
+ dragReorder(dragState);
416
+ }
417
+ else {
418
+ dragMove(dragState, metaKey
419
+ ? [dragState.element]
420
+ : [dragState.element, dragState.initialContainer]);
421
+ }
422
+ dragState.element.style.setProperty('translate', `${x - dragState.offset.x}px ${y - dragState.offset.y}px`);
423
+ return;
424
+ }
425
+ case 'click':
426
+ case 'dblclick':
427
+ if (mode === 'test' || !component) {
428
+ return;
429
+ }
430
+ const { x, y, type } = message.data;
431
+ const elementsAtPoint = document.elementsFromPoint(x, y);
432
+ let element = elementsAtPoint.find((elem) => {
433
+ const id = elem.getAttribute('data-id');
434
+ if (typeof id !== 'string' ||
435
+ component === null ||
436
+ elem.getAttribute('data-component')) {
437
+ return false;
438
+ }
439
+ const nodeId = getNodeId(component, id.split('.').slice(1));
440
+ const node = nodeId ? component?.nodes[nodeId] : undefined;
441
+ if (!node) {
442
+ return false;
443
+ }
444
+ if (elem.getAttribute('data-node-type') === 'text') {
445
+ return (
446
+ // Select text nodes if the meta key is pressed or the text node is double-clicked
447
+ metaKey ||
448
+ type === 'dblclick' ||
449
+ // Select text nodes if the selected node is a text node. This is useful as the user is likely in a text editing mode
450
+ getDOMNodeFromNodeId(selectedNodeId)?.getAttribute('data-node-type') === 'text');
451
+ }
452
+ return true;
453
+ });
454
+ // Bubble selection to the topmost parent that has the exact same size as the element.
455
+ // This is important for drag and drop as you are often left with childless parents after dragging.
456
+ while (element?.parentElement &&
457
+ element.getAttribute('data-node-id') !== 'root' &&
458
+ fastDeepEqual(element.getBoundingClientRect().toJSON(), element.parentElement.getBoundingClientRect().toJSON()) &&
459
+ element.getAttribute('data-node-type') !== 'text') {
460
+ element = element.parentElement;
461
+ }
462
+ const id = element?.getAttribute('data-id') ?? null;
463
+ if (type === 'click' && id !== selectedNodeId) {
464
+ if (message.data.metaKey) {
465
+ // Figure out if the clicked element is a text element
466
+ // or if one of its descendants is a text element
467
+ const root = component.nodes.root;
468
+ if (root && id) {
469
+ const nodeLookup = getNodeAndAncestors(component, root, id);
470
+ if (nodeLookup?.node.type === 'text') {
471
+ window.parent?.postMessage({
472
+ type: 'selection',
473
+ selectedNodeId: id,
474
+ }, '*');
475
+ }
476
+ else {
477
+ const firstTextChild = nodeLookup?.node.type === 'element'
478
+ ? nodeLookup.node.children.find((c) => component?.nodes[c]?.type === 'text')
479
+ : undefined;
480
+ if (firstTextChild) {
481
+ window.parent?.postMessage({
482
+ type: 'selection',
483
+ selectedNodeId: `${id}.0`,
484
+ }, '*');
485
+ }
486
+ }
487
+ }
488
+ }
489
+ else {
490
+ window.parent?.postMessage({
491
+ type: 'selection',
492
+ selectedNodeId: id,
493
+ }, '*');
494
+ }
495
+ }
496
+ else if (type === 'mousemove' && id !== highlightedNodeId) {
497
+ window.parent?.postMessage({
498
+ type: 'highlight',
499
+ highlightedNodeId: id,
500
+ }, '*');
501
+ }
502
+ else if (type === 'dblclick' &&
503
+ id &&
504
+ // We only allow dblclick --> navigation if we're not in test mode
505
+ mode === 'design') {
506
+ // Figure out if the clicked element is a component
507
+ const root = component.nodes.root;
508
+ if (root) {
509
+ const nodeLookup = getNodeAndAncestors(component, root, id);
510
+ if (nodeLookup?.node.type === 'component' &&
511
+ nodeLookup.node.name) {
512
+ window.parent?.postMessage({
513
+ type: 'navigate',
514
+ name: nodeLookup.node.name,
515
+ }, '*');
516
+ }
517
+ // Double click on text node should select the text node for editing
518
+ else if (nodeLookup?.node.type === 'text') {
519
+ window.parent?.postMessage({
520
+ type: 'selection',
521
+ selectedNodeId: id,
522
+ }, '*');
523
+ }
524
+ }
525
+ }
526
+ break;
527
+ case 'style_variant_changed':
528
+ const { variantIndex } = message.data;
529
+ updateSelectedStyleVariant(variantIndex);
530
+ break;
531
+ // We request manually instead of automatic to avoid mutation observer spam.
532
+ // Also, reporting automatically proved unreliable when elements' height was in %
533
+ case 'report_document_scroll_size':
534
+ window.parent?.postMessage({
535
+ type: 'documentScrollSize',
536
+ scrollHeight: domNode.scrollHeight,
537
+ scrollWidth: domNode.scrollWidth,
538
+ }, '*');
539
+ break;
540
+ case 'reload':
541
+ window.location.reload();
542
+ break;
543
+ case 'fetch_api':
544
+ const { apiKey } = message.data;
545
+ dataSignal.update((data) => ({
546
+ ...data,
547
+ Apis: {
548
+ ...data.Apis,
549
+ [apiKey]: {
550
+ isLoading: true,
551
+ data: null,
552
+ error: null,
553
+ },
554
+ },
555
+ }));
556
+ ctx?.apis[apiKey]?.fetch({});
557
+ break;
558
+ case 'drag-started':
559
+ const draggedElement = getDOMNodeFromNodeId(selectedNodeId);
560
+ if (!draggedElement || !draggedElement.parentElement) {
561
+ return;
562
+ }
563
+ const repeatedNodes = Array.from(draggedElement.parentElement.children).filter((node) => node instanceof HTMLElement &&
564
+ node.getAttribute('data-id')?.startsWith(selectedNodeId + '('));
565
+ dragState = dragStarted({
566
+ element: draggedElement,
567
+ lastCursorPosition: { x: message.data.x, y: message.data.y },
568
+ repeatedNodes,
569
+ asCopy: altKey,
570
+ });
571
+ if (altKey) {
572
+ const nextRect = dragState.element.getBoundingClientRect();
573
+ dragState.offset.x += nextRect.left - dragState.initialRect.left;
574
+ dragState.offset.y += nextRect.top - dragState.initialRect.top;
575
+ }
576
+ break;
577
+ case 'drag-ended':
578
+ switch (dragState?.mode) {
579
+ case 'reorder':
580
+ const parentDataId = dragState?.initialContainer.getAttribute('data-id');
581
+ const parentNodeId = dragState?.initialContainer.getAttribute('data-node-id');
582
+ if (!parentDataId || !parentNodeId) {
583
+ return;
584
+ }
585
+ const nextSibling = dragState?.element.nextElementSibling;
586
+ const nextSiblingId = parseInt(nextSibling?.getAttribute('data-id')?.split('.').at(-1) ?? '');
587
+ const rect = dragState?.element?.getBoundingClientRect();
588
+ if (rect &&
589
+ !message.data.canceled &&
590
+ (nextSibling !== dragState?.initialNextSibling ||
591
+ dragState?.copy)) {
592
+ void dragEnded(dragState, false).then(() => {
593
+ window.parent?.postMessage({
594
+ type: 'nodeMoved',
595
+ copy: Boolean(dragState?.copy),
596
+ parent: parentDataId,
597
+ index: !isNaN(nextSiblingId)
598
+ ? nextSiblingId
599
+ : component?.nodes[parentNodeId]?.children?.length,
600
+ }, '*');
601
+ dragState = null;
602
+ });
603
+ }
604
+ else {
605
+ void dragEnded(dragState, true).then(() => {
606
+ dragState = null;
607
+ });
608
+ }
609
+ break;
610
+ case 'insert':
611
+ const selectedPermutation = dragState?.insertAreas?.[dragState?.selectedInsertAreaIndex ?? -1];
612
+ if (selectedPermutation && !message.data.canceled) {
613
+ void dragEnded(dragState, false).then(() => {
614
+ window.parent?.postMessage({
615
+ type: 'nodeMoved',
616
+ copy: Boolean(dragState?.copy),
617
+ parent: selectedPermutation?.parent.getAttribute('data-id'),
618
+ index: selectedPermutation?.index,
619
+ }, '*');
620
+ dragState = null;
621
+ });
622
+ }
623
+ else {
624
+ void dragEnded(dragState, true).then(() => {
625
+ dragState = null;
626
+ });
627
+ }
628
+ break;
629
+ }
630
+ break;
631
+ case 'keydown':
632
+ case 'keyup':
633
+ // If the `altKey` is pressed/released and the user is currently dragging, then restart the drag with/without a copy.
634
+ if (dragState &&
635
+ !dragState.destroying &&
636
+ message.data.altKey !== altKey) {
637
+ const asCopy = message.data.altKey;
638
+ const prevRect = dragState.element.getBoundingClientRect();
639
+ void dragEnded(dragState, true).then(() => {
640
+ if (!dragState)
641
+ return;
642
+ dragState = dragStarted({
643
+ element: dragState.element,
644
+ lastCursorPosition: dragState.lastCursorPosition,
645
+ repeatedNodes: dragState.repeatedNodes,
646
+ asCopy,
647
+ initialContainer: dragState.initialContainer,
648
+ initialNextSibling: dragState.initialNextSibling,
649
+ });
650
+ const nextRect = dragState.element.getBoundingClientRect();
651
+ dragState.offset.x += nextRect.left - prevRect.left;
652
+ dragState.offset.y += nextRect.top - prevRect.top;
653
+ });
654
+ }
655
+ altKey = message.data.altKey;
656
+ metaKey = message.data.metaKey;
657
+ break;
658
+ case 'get_computed_style':
659
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
660
+ if (!selectedNode) {
661
+ return;
662
+ }
663
+ const { styles } = message.data;
664
+ const computedStyle = window.getComputedStyle(selectedNode);
665
+ window.parent?.postMessage({
666
+ type: 'computedStyle',
667
+ computedStyle: Object.fromEntries(styles.map((style) => [
668
+ style,
669
+ computedStyle.getPropertyValue(style),
670
+ ])),
671
+ }, '*');
672
+ break;
673
+ case 'set_timeline_keyframes':
674
+ const { keyframes } = message.data;
675
+ document.head.querySelector('[data-timeline-keyframes]')?.remove();
676
+ if (!keyframes) {
677
+ return;
678
+ }
679
+ const styleElem = document.createElement('style');
680
+ styleElem.appendChild(document.createTextNode(`
681
+ @keyframes preview_timeline {
682
+ ${Object.values(keyframes)
683
+ .map(({ key, value, position, easing }) => `${position * 100}% {
684
+ ${key}: ${value};
685
+ ${easing ? `animation-timing-function: ${easing};` : ''}
686
+ }`)
687
+ .join('\n')}
688
+ }
689
+ `));
690
+ styleElem.setAttribute('data-timeline-keyframes', '');
691
+ document.head.appendChild(styleElem);
692
+ window.parent?.postMessage({
693
+ type: 'selectionRect',
694
+ rect: getRectData(getDOMNodeFromNodeId(selectedNodeId) ?? document.body),
695
+ }, '*');
696
+ break;
697
+ case 'set_timeline_time':
698
+ const { time, timingFunction, fillMode } = message.data;
699
+ const prevAnimatedElement = getDOMNodeFromNodeId(animationState?.animatedElementId ?? '');
700
+ animationState = {
701
+ animatedElementId: time !== null ? selectedNodeId : null,
702
+ time,
703
+ timingFunction,
704
+ fillMode,
705
+ };
706
+ const animatedElement = getDOMNodeFromNodeId(animationState.animatedElementId);
707
+ if (prevAnimatedElement === null ||
708
+ prevAnimatedElement !== animatedElement) {
709
+ prevAnimatedElement?.classList.remove('editor-preview-timeline');
710
+ }
711
+ if (animatedElement && time !== null) {
712
+ animatedElement.classList.add('editor-preview-timeline');
713
+ document.body.style.setProperty('--editor-timeline-position', `${time}s`);
714
+ document.body.style.setProperty('--editor-timeline-timing-function', timingFunction ?? 'ease');
715
+ document.body.style.setProperty('--editor-timeline-fill-mode', fillMode ?? 'none');
716
+ }
717
+ else {
718
+ document.body.style.removeProperty('--editor-timeline-position');
719
+ document.body.style.removeProperty('--editor-timeline-timing-function');
720
+ document.body.style.removeProperty('--editor-timeline-fill-mode');
721
+ update();
722
+ }
723
+ window.parent?.postMessage({
724
+ type: 'selectionRect',
725
+ rect: getRectData(animatedElement),
726
+ }, '*');
727
+ break;
728
+ case 'preview_style':
729
+ const { styles: previewStyleStyles } = message.data;
730
+ cancelAnimationFrame(previewStyleAnimationFrame);
731
+ previewStyleAnimationFrame = requestAnimationFrame(() => {
732
+ // Update or create a new style tag and set the given styles with important priority
733
+ let styleTag = document.head.querySelector('[data-id="selected-node-styles"]');
734
+ // Cleanup when null styles are sent
735
+ if (!previewStyleStyles) {
736
+ styleTag?.remove();
737
+ return;
738
+ }
739
+ if (!styleTag) {
740
+ styleTag = document.createElement('style');
741
+ styleTag.setAttribute('data-id', 'selected-node-styles');
742
+ document.head.appendChild(styleTag);
743
+ }
744
+ const previewStyles = Object.entries(previewStyleStyles)
745
+ .map(([key, value]) => `${key}: ${value} !important;`)
746
+ .join('\n');
747
+ styleTag.innerHTML = `[data-id="${selectedNodeId}"], [data-id="${selectedNodeId}"] ~ [data-id^="${selectedNodeId}("] {
748
+ ${previewStyles}
749
+ transition: none !important;
750
+ }`;
751
+ window.parent?.postMessage({
752
+ type: 'selectionRect',
753
+ rect: getRectData(getDOMNodeFromNodeId(selectedNodeId)),
754
+ }, '*');
755
+ });
756
+ break;
757
+ }
758
+ });
759
+ const updateStyle = () => {
760
+ if (component) {
761
+ insertStyles(document.head, component, getAllComponents());
762
+ }
763
+ };
764
+ /**
765
+ * Get the current representation of the component, but with
766
+ * updated conditions based on selectedNodeId and updated
767
+ * styling based on styleVariantSelection
768
+ */
769
+ const getCurrentComponent = () => {
770
+ const _component = structuredClone(component);
771
+ if (!_component) {
772
+ return null;
773
+ }
774
+ if (mode === 'design') {
775
+ if (selectedNodeId !== null) {
776
+ const root = _component?.nodes.root;
777
+ if (root) {
778
+ const nodeLookup = getNodeAndAncestors(_component, root, selectedNodeId);
779
+ if (nodeLookup) {
780
+ if (isNodeOrAncestorConditional(nodeLookup)) {
781
+ // Show the selected node and all its ancestors by
782
+ // removing their "show" condition
783
+ nodeLookup.node.condition = undefined;
784
+ nodeLookup.ancestors.forEach((a) => (a.condition = undefined));
785
+ }
786
+ }
787
+ }
788
+ }
789
+ }
790
+ return _component;
791
+ };
792
+ const updateSelectedStyleVariant = (variantIndex) => {
793
+ clearSelectedStyleVariant();
794
+ if (selectedNodeId !== null && typeof variantIndex === 'number') {
795
+ styleVariantSelection = {
796
+ nodeId: selectedNodeId,
797
+ styleVariantIndex: variantIndex,
798
+ };
799
+ const root = component?.nodes.root;
800
+ if (root && component) {
801
+ const nodeLookup = getNodeAndAncestors(component, root, selectedNodeId);
802
+ if (nodeLookup) {
803
+ if (styleVariantSelection?.nodeId === selectedNodeId &&
804
+ (nodeLookup.node.type === 'element' ||
805
+ nodeLookup.node.type === 'component')) {
806
+ const selectedStyleVariant = nodeLookup.node.variants?.[styleVariantSelection.styleVariantIndex] ?? { style: {} };
807
+ // Add a style element specific to the selected element which
808
+ // is only applied when the preview is in design mode
809
+ const styleElem = document.createElement('style');
810
+ styleElem.setAttribute('data-hash', selectedNodeId);
811
+ styleElem.appendChild(document.createTextNode(`
812
+ body[data-mode="design"] [data-id="${selectedNodeId}"] {
813
+ ${styleToCss({
814
+ ...nodeLookup.node.style,
815
+ ...selectedStyleVariant.style,
816
+ })}
817
+ }
818
+ `));
819
+ const existingStyleElement = document.head.querySelector(`[data-hash="${selectedNodeId}"]`);
820
+ if (existingStyleElement) {
821
+ document.head.removeChild(existingStyleElement);
822
+ }
823
+ document.head.appendChild(styleElem);
824
+ }
825
+ }
826
+ }
827
+ }
828
+ const selectedNode = getDOMNodeFromNodeId(selectedNodeId);
829
+ window.parent?.postMessage({
830
+ type: 'selectionRect',
831
+ rect: getRectData(selectedNode),
832
+ }, '*');
833
+ };
834
+ const update = () => {
835
+ const _component = getCurrentComponent();
836
+ if (!_component || !components || !packageComponents) {
837
+ return;
838
+ }
839
+ let { Attributes, Variables, Contexts } = dataSignal.get();
840
+ if (fastDeepEqual(ctx?.component.attributes, _component.attributes) === false) {
841
+ Attributes = mapObject(_component.attributes, ([name, { testValue }]) => [
842
+ name,
843
+ testValue,
844
+ ]);
845
+ }
846
+ if (_component.route &&
847
+ fastDeepEqual(ctx?.component.route, _component.route) === false) {
848
+ // Subscribe to the route signal so we can preview URL parameter changes in the editor
849
+ routeSignal?.destroy();
850
+ if (_component.route) {
851
+ // Populate initial URL parameters with test data
852
+ window.toddle.locationSignal.update((location) => {
853
+ if (!_component.route)
854
+ return location;
855
+ return {
856
+ ...location,
857
+ route: _component.route,
858
+ params: Object.fromEntries(_component.route.path
859
+ .filter((p) => p.type === 'param')
860
+ .map((p) => [p.name, p.testValue])),
861
+ query: mapObject(_component.route.query, ([name, { testValue }]) => [
862
+ name,
863
+ testValue,
864
+ ]),
865
+ };
866
+ });
867
+ routeSignal = window.toddle.locationSignal.map(({ query, params }) => {
868
+ return { ...query, ...params };
869
+ });
870
+ routeSignal.subscribe((route) => dataSignal.update((data) => ({
871
+ ...data,
872
+ 'URL parameters': route,
873
+ Attributes: route,
874
+ })));
875
+ }
876
+ Attributes = mapObject(_component.attributes, ([name, { testValue }]) => [
877
+ name,
878
+ testValue,
879
+ ]);
880
+ }
881
+ if (fastDeepEqual(ctx?.component.route?.info?.meta, _component.route?.info?.meta) === false) {
882
+ insertHeadTags(_component.route?.info?.meta ?? {}, {
883
+ component: _component,
884
+ data: { Attributes },
885
+ root: document,
886
+ package: ctx?.package,
887
+ toddle: window.toddle,
888
+ env,
889
+ });
890
+ }
891
+ if (fastDeepEqual(_component.contexts, ctx?.component.contexts) === false) {
892
+ Contexts = (function createStaticContextFromComponent(component, contextProvidersCreated) {
893
+ contextProvidersCreated?.add(component.name);
894
+ return mapObject(component.contexts ?? {}, ([providerName, context]) => {
895
+ if (contextProvidersCreated?.has(providerName)) {
896
+ // Circular dependency detected in context-providers (ie. A -> B -> A -> ...), stop recursion
897
+ return [providerName, {}];
898
+ }
899
+ const providerComponent = getAllComponents().find((c) => c.name === providerName);
900
+ if (!providerComponent) {
901
+ console.warn(`Could not find a provider-component named "${providerName}" in files`);
902
+ return [providerName, {}];
903
+ }
904
+ // TODO: Should we also run APIs for the provider?
905
+ const formulaContext = {
906
+ data: {
907
+ Attributes: mapObject(providerComponent.attributes, ([name, attr]) => [name, attr.testValue]),
908
+ // Recursively resolve contexts providers before their children to build up the fake context tree in preview mode
909
+ Contexts: createStaticContextFromComponent(providerComponent, contextProvidersCreated ?? new Set()),
910
+ },
911
+ component: providerComponent,
912
+ root: ctx?.root,
913
+ formulaCache: {},
914
+ package: ctx?.package,
915
+ toddle: window.toddle,
916
+ env,
917
+ };
918
+ // Pages can also be context-providers!
919
+ // Exposed formulas can derive their preview output from URL data,
920
+ // so we must populate Url parameters with their test data
921
+ if (providerComponent.route) {
922
+ formulaContext.data['URL parameters'] = {
923
+ ...Object.fromEntries(providerComponent.route.path
924
+ .filter((p) => p.type === 'param')
925
+ .map((p) => [p.name, p.testValue])),
926
+ ...mapObject(providerComponent.route.query, ([name, { testValue }]) => [name, testValue]),
927
+ };
928
+ }
929
+ formulaContext.data.Variables = mapObject(providerComponent.variables, ([name, variable]) => [
930
+ name,
931
+ applyFormula(variable.initialValue, formulaContext),
932
+ ]);
933
+ return [
934
+ providerName,
935
+ Object.fromEntries(context.formulas.map((formulaName) => {
936
+ const formula = providerComponent.formulas?.[formulaName];
937
+ if (!formula) {
938
+ console.warn(`Could not find formula "${formulaName}" in component "${providerName}"`);
939
+ return [formulaName, null];
940
+ }
941
+ return [
942
+ formulaName,
943
+ applyFormula(formula.formula, formulaContext),
944
+ ];
945
+ })),
946
+ ];
947
+ });
948
+ })(_component);
949
+ }
950
+ if (fastDeepEqual(_component.variables, ctx?.component.variables) === false) {
951
+ Variables = mapObject(_component.variables, ([name, { initialValue }]) => [
952
+ name,
953
+ applyFormula(initialValue, {
954
+ data: { Attributes, Contexts },
955
+ component: _component,
956
+ root: document,
957
+ package: ctx?.package,
958
+ toddle: window.toddle,
959
+ env,
960
+ }),
961
+ ]);
962
+ }
963
+ dataSignal.update((data) => {
964
+ return {
965
+ ...data,
966
+ 'URL parameters': component && isPageComponent(component)
967
+ ? {
968
+ ...window.toddle.locationSignal.get().query,
969
+ ...window.toddle.locationSignal.get().params,
970
+ }
971
+ : {},
972
+ Attributes,
973
+ Variables,
974
+ Contexts,
975
+ };
976
+ });
977
+ const newCtx = {
978
+ ...(ctx ?? createContext(_component, getAllComponents())),
979
+ component: _component,
980
+ };
981
+ for (const api in newCtx.component.apis) {
982
+ // check if the api has changed (ignoring onCompleted and onFailed).
983
+ const apiInstance = newCtx.component.apis[api];
984
+ const previousApiInstance = ctx?.component.apis[api];
985
+ if (isLegacyApi(apiInstance)) {
986
+ if (fastDeepEqual(omitKeys(apiInstance, ['onCompleted', 'onFailed']), previousApiInstance && isLegacyApi(previousApiInstance)
987
+ ? omitKeys(previousApiInstance, ['onCompleted', 'onFailed'])
988
+ : (previousApiInstance ?? {})) === false) {
989
+ newCtx.apis[api]?.destroy();
990
+ dataSignal.update((data) => {
991
+ return {
992
+ ...data,
993
+ Apis: omitKeys(data.Apis ?? {}, [
994
+ ...Object.keys(data.Apis ?? {}).filter(
995
+ // remove any data from an api that is not part of the component
996
+ (key) => !newCtx.component.apis[key]),
997
+ api,
998
+ ]),
999
+ };
1000
+ });
1001
+ newCtx.apis[api] = createLegacyAPI(apiInstance, newCtx);
1002
+ }
1003
+ }
1004
+ else {
1005
+ if (!newCtx.apis[api]) {
1006
+ newCtx.apis[api] = createAPI(apiInstance, newCtx);
1007
+ }
1008
+ else {
1009
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
1010
+ newCtx.apis[api].update && newCtx.apis[api].update(apiInstance);
1011
+ }
1012
+ }
1013
+ }
1014
+ if (fastDeepEqual(newCtx.component.nodes, ctx?.component?.nodes) === false) {
1015
+ updateStyle();
1016
+ // Remove preview styles automatically when the component changes
1017
+ document.head.querySelector('[data-id="selected-node-styles"]')?.remove();
1018
+ if (fastDeepEqual(omitSubnodeStyleForComponent(newCtx.component), omitSubnodeStyleForComponent(ctx?.component))) {
1019
+ // If we're in here, then the latest update was only a style change, so we should try some optimistic updates
1020
+ Object.keys(newCtx.component.nodes).forEach((nodeId) => {
1021
+ const newNode = newCtx.component.nodes[nodeId];
1022
+ const oldNode = ctx?.component.nodes[nodeId];
1023
+ if ((newNode.type === 'element' || newNode.type === 'component') &&
1024
+ (oldNode?.type === 'element' || oldNode?.type === 'component') &&
1025
+ (!fastDeepEqual(newNode.style, oldNode.style) ||
1026
+ !fastDeepEqual(newNode.variants, oldNode.variants))) {
1027
+ document
1028
+ .querySelectorAll(`[data-node-id="${nodeId}"]`)
1029
+ .forEach((nodeInstance) => {
1030
+ nodeInstance.classList.remove(getClassName([oldNode.style, oldNode.variants]));
1031
+ nodeInstance.classList.add(getClassName([newNode.style, newNode.variants]));
1032
+ });
1033
+ }
1034
+ });
1035
+ }
1036
+ else {
1037
+ Array.from(domNode.children).forEach((child) => {
1038
+ if (child.tagName !== 'SCRIPT') {
1039
+ child.remove();
1040
+ }
1041
+ });
1042
+ // Clear old root signal and create a new one to not keep old signals with previous root around
1043
+ ctxDataSignal?.destroy();
1044
+ ctxDataSignal = dataSignal.map((data) => data);
1045
+ const rootElem = createNode({
1046
+ id: 'root',
1047
+ path: '0',
1048
+ dataSignal: ctxDataSignal,
1049
+ ctx: newCtx,
1050
+ parentElement: domNode,
1051
+ instance: { [newCtx.component.name]: 'root' },
1052
+ });
1053
+ newCtx.component.onLoad?.actions.forEach((action) => {
1054
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1055
+ handleAction(action, dataSignal.get(), newCtx);
1056
+ });
1057
+ rootElem.forEach((elem) => domNode.appendChild(elem));
1058
+ window.parent?.postMessage({
1059
+ type: 'style',
1060
+ time: new Intl.DateTimeFormat('en-GB', {
1061
+ timeStyle: 'long',
1062
+ }).format(new Date()),
1063
+ }, '*');
1064
+ }
1065
+ }
1066
+ // Rerendering may clear editor-preview-only styles, so we need to reapply them
1067
+ getDOMNodeFromNodeId(animationState?.animatedElementId)?.classList.add('editor-preview-timeline');
1068
+ ctx = newCtx;
1069
+ };
1070
+ const createContext = (component, components) => {
1071
+ const ctx = {
1072
+ component,
1073
+ components,
1074
+ triggerEvent: (event, data) => {
1075
+ window.parent?.postMessage({
1076
+ type: 'component event',
1077
+ event,
1078
+ time: new Intl.DateTimeFormat('en-GB', {
1079
+ timeStyle: 'long',
1080
+ }).format(new Date()),
1081
+ data,
1082
+ }, '*');
1083
+ },
1084
+ dataSignal,
1085
+ root: document,
1086
+ isRootComponent: true,
1087
+ apis: {},
1088
+ children: {},
1089
+ abortSignal: new AbortController().signal,
1090
+ formulaCache: createFormulaCache(component),
1091
+ providers: {},
1092
+ package: undefined,
1093
+ toddle: window.toddle,
1094
+ env,
1095
+ };
1096
+ if (isContextProvider(component)) {
1097
+ // Subscribe to exposed formulas and update the component's data signal
1098
+ const formulaDataSignals = Object.fromEntries(Object.entries(component.formulas ?? {})
1099
+ .filter(([, formula]) => formula.exposeInContext)
1100
+ .map(([name, formula]) => [
1101
+ name,
1102
+ dataSignal.map((data) => applyFormula(formula.formula, {
1103
+ data,
1104
+ component,
1105
+ formulaCache: ctx.formulaCache,
1106
+ root: ctx.root,
1107
+ package: ctx.package,
1108
+ toddle: window.toddle,
1109
+ env,
1110
+ })),
1111
+ ]));
1112
+ ctx.providers = {
1113
+ ...ctx.providers,
1114
+ [component.name]: {
1115
+ component,
1116
+ formulaDataSignals,
1117
+ ctx,
1118
+ },
1119
+ };
1120
+ }
1121
+ return ctx;
1122
+ };
1123
+ document.addEventListener('keydown', (event) => {
1124
+ if (isInputTarget(event)) {
1125
+ return;
1126
+ }
1127
+ switch (event.key) {
1128
+ case 'k':
1129
+ if (event.metaKey) {
1130
+ event.preventDefault();
1131
+ }
1132
+ }
1133
+ window.parent?.postMessage({
1134
+ type: 'keydown',
1135
+ event: {
1136
+ key: event.key,
1137
+ metaKey: event.metaKey,
1138
+ shiftKey: event.shiftKey,
1139
+ altKey: event.altKey,
1140
+ },
1141
+ }, '*');
1142
+ });
1143
+ document.addEventListener('keyup', (event) => {
1144
+ if (isInputTarget(event)) {
1145
+ return;
1146
+ }
1147
+ window.parent?.postMessage({
1148
+ type: 'keyup',
1149
+ event: {
1150
+ key: event.key,
1151
+ metaKey: event.metaKey,
1152
+ shiftKey: event.shiftKey,
1153
+ altKey: event.altKey,
1154
+ },
1155
+ }, '*');
1156
+ });
1157
+ document.addEventListener('keypress', (event) => {
1158
+ if (isInputTarget(event)) {
1159
+ return;
1160
+ }
1161
+ window.parent?.postMessage({
1162
+ type: 'keypress',
1163
+ event: {
1164
+ key: event.key,
1165
+ metaKey: event.metaKey,
1166
+ shiftKey: event.shiftKey,
1167
+ altKey: event.altKey,
1168
+ },
1169
+ }, '*');
1170
+ });
1171
+ dataSignal.subscribe((data) => {
1172
+ if (component && components && packageComponents && data) {
1173
+ try {
1174
+ window.parent?.postMessage({ type: 'data', data }, '*');
1175
+ }
1176
+ catch {
1177
+ // If we're unable to send the data, let's try to JSON serialize it
1178
+ window.parent?.postMessage({ type: 'data', data: JSON.parse(JSON.stringify(data)) }, '*');
1179
+ }
1180
+ }
1181
+ });
1182
+ const clearSelectedStyleVariant = () => {
1183
+ if (styleVariantSelection) {
1184
+ const styleElem = document.head.querySelector(`[data-hash="${styleVariantSelection.nodeId}"]`);
1185
+ if (styleElem) {
1186
+ document.head.removeChild(styleElem);
1187
+ }
1188
+ styleVariantSelection = null;
1189
+ }
1190
+ };
1191
+ const updateConditionalElements = () => {
1192
+ const displayedNodes = [];
1193
+ if (selectedNodeId && component) {
1194
+ const root = component.nodes.root;
1195
+ if (root) {
1196
+ const nodeLookup = getNodeAndAncestors(component, root, selectedNodeId);
1197
+ if (isNodeOrAncestorConditional(nodeLookup)) {
1198
+ displayedNodes.push(selectedNodeId);
1199
+ displayedNodes.push(...[...nodeLookup.ancestors, nodeLookup.node]
1200
+ .filter((a) => a.condition)
1201
+ .map((a) => a.nodeId));
1202
+ }
1203
+ }
1204
+ }
1205
+ showSignal.set({
1206
+ displayedNodes,
1207
+ testMode: mode === 'test',
1208
+ });
1209
+ };
1210
+ };
1211
+ const insertOrReplaceHeadNode = (id, node) => {
1212
+ const existing = document.head.querySelector(`[data-meta-id="${id}"]`);
1213
+ if (existing) {
1214
+ existing.replaceWith(node);
1215
+ }
1216
+ else {
1217
+ document.head.appendChild(node);
1218
+ }
1219
+ };
1220
+ const insertHeadTags = (entries, context) => {
1221
+ // Remove all tags that has a data-meta-id attribute that is not in the entries
1222
+ Array.from(document.head.querySelectorAll('[data-meta-id]'))
1223
+ .filter((elem) => !entries[elem.getAttribute('data-meta-id')])
1224
+ .forEach((elem) => elem.remove());
1225
+ // Skip anything that is not <link> or <script> tags, as they don't have any influence on the preview
1226
+ Object.entries(entries).forEach(([id, entry]) => {
1227
+ switch (entry.tag) {
1228
+ case 'link':
1229
+ return insertOrReplaceHeadNode(id, document.createRange().createContextualFragment(`
1230
+ <link
1231
+ data-meta-id="${id}"
1232
+ ${Object.entries(entry.attrs)
1233
+ .map(([key, value]) => `${key}="${applyFormula(value, context)}"`)
1234
+ .join(' ')}
1235
+ />
1236
+ `));
1237
+ case 'script':
1238
+ return insertOrReplaceHeadNode(id, document.createRange().createContextualFragment(`
1239
+ <script
1240
+ data-meta-id="${id}"
1241
+ ${Object.entries(entry.attrs)
1242
+ .map(([key, value]) => `${key}="${applyFormula(value, context)}"`)
1243
+ .join(' ')}
1244
+ ></script>
1245
+ `));
1246
+ }
1247
+ });
1248
+ };
1249
+ export function getDOMNodeFromNodeId(selectedNodeId) {
1250
+ if (!selectedNodeId) {
1251
+ return null;
1252
+ }
1253
+ return document.querySelector(`[data-id="${selectedNodeId}"]:not([data-component])`);
1254
+ }
1255
+ export function getRectData(selectedNode) {
1256
+ if (!selectedNode) {
1257
+ return null;
1258
+ }
1259
+ const rect = selectedNode.getBoundingClientRect();
1260
+ return {
1261
+ left: rect.left,
1262
+ right: rect.right,
1263
+ top: rect.top,
1264
+ bottom: rect.bottom,
1265
+ width: rect.width,
1266
+ height: rect.height,
1267
+ x: rect.x,
1268
+ y: rect.y,
1269
+ borderRadius: window
1270
+ .getComputedStyle(selectedNode)
1271
+ .borderRadius.split(' ')
1272
+ .map(parseFloat),
1273
+ };
1274
+ }
1275
+ function getNodeId(component, path) {
1276
+ function getId([nextChild, ...path], currentId) {
1277
+ if (nextChild === undefined || currentId === undefined) {
1278
+ return currentId ?? null;
1279
+ }
1280
+ const currentNode = component.nodes[currentId];
1281
+ if (!currentNode?.children) {
1282
+ return null;
1283
+ }
1284
+ // We only allow selecting the first element in a repeat (which does not have a repeat-index "()")
1285
+ if (nextChild.endsWith(')')) {
1286
+ return null;
1287
+ }
1288
+ return getId(path, currentNode.children[parseInt(nextChild)]);
1289
+ }
1290
+ return getId(path, 'root');
1291
+ }
1292
+ const insertTheme = (parent, theme) => {
1293
+ document.getElementById('theme-style')?.remove();
1294
+ const styleElem = document.createElement('style');
1295
+ styleElem.setAttribute('type', 'text/css');
1296
+ styleElem.setAttribute('id', 'theme-style');
1297
+ styleElem.innerHTML = getThemeCss(theme, {
1298
+ includeResetStyle: false,
1299
+ createFontFaces: true,
1300
+ });
1301
+ parent.appendChild(styleElem);
1302
+ };
1303
+ //# sourceMappingURL=editor-preview.main.js.map