@nordcraft/runtime 1.0.58 → 1.0.60

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.
@@ -3,9 +3,9 @@
3
3
  /* eslint-disable no-case-declarations */
4
4
  /* eslint-disable no-fallthrough */
5
5
  import { isLegacyApi } from '@nordcraft/core/dist/api/api'
6
+ import { isLegacyPluginAction } from '@nordcraft/core/dist/component/actionUtils'
6
7
  import {
7
8
  HeadTagTypes,
8
- type AnimationKeyframe,
9
9
  type Component,
10
10
  type ComponentData,
11
11
  type MetaEntry,
@@ -24,8 +24,6 @@ import {
24
24
  type PluginFormula,
25
25
  type ToddleFormula,
26
26
  } from '@nordcraft/core/dist/formula/formulaTypes'
27
- import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
28
- import { getClassName } from '@nordcraft/core/dist/styling/className'
29
27
  import { appendUnit } from '@nordcraft/core/dist/styling/customProperty'
30
28
  import type { OldTheme, Theme } from '@nordcraft/core/dist/styling/theme'
31
29
  import { getThemeCss, renderTheme } from '@nordcraft/core/dist/styling/theme'
@@ -57,7 +55,15 @@ import { dragMove } from './editor/drag-drop/dragMove'
57
55
  import { dragReorder } from './editor/drag-drop/dragReorder'
58
56
  import { dragStarted } from './editor/drag-drop/dragStarted'
59
57
  import { introspectApiRequest } from './editor/graphql'
60
- import type { DragState } from './editor/types'
58
+ import { isInputTarget } from './editor/input'
59
+ import { updateComponentLinks } from './editor/links'
60
+ import { getRectData } from './editor/overlay'
61
+ import {
62
+ TextNodeComputedStyles,
63
+ type DragState,
64
+ type EditorPostMessageType,
65
+ type NordcraftPreviewEvent,
66
+ } from './editor/types'
61
67
  import { handleAction } from './events/handleAction'
62
68
  import type { Signal } from './signal/signal'
63
69
  import { signal } from './signal/signal'
@@ -70,150 +76,12 @@ import type {
70
76
  } from './types'
71
77
  import { createFormulaCache } from './utils/createFormulaCache'
72
78
  import { getNodeAndAncestors, isNodeOrAncestorConditional } from './utils/nodes'
73
- import { omitSubnodeStyleForComponent } from './utils/omitStyle'
74
79
  import { rectHasPoint } from './utils/rectHasPoint'
75
80
  import {
76
81
  getScrollStateRestorer,
77
82
  storeScrollState,
78
83
  } from './utils/storeScrollState'
79
84
 
80
- type ToddlePreviewEvent =
81
- | {
82
- type: 'style_variant_changed'
83
- variantIndex: number | null
84
- }
85
- | {
86
- type: 'component'
87
- component: Component
88
- }
89
- | { type: 'components'; components: Component[] }
90
- | {
91
- type: 'packages'
92
- packages: Record<
93
- string,
94
- {
95
- components: Record<string, Component>
96
- formulas: Record<
97
- string,
98
- PluginFormula<FormulaHandlerV2> | PluginFormula<string>
99
- >
100
- actions: Record<string, PluginActionV2 | PluginAction>
101
- manifest: {
102
- name: string
103
- // commit represents the commit hash (version) of the package
104
- commit: string
105
- }
106
- }
107
- >
108
- }
109
- | {
110
- type: 'global_formulas'
111
- formulas: Record<
112
- string,
113
- PluginFormula<FormulaHandlerV2> | PluginFormula<string>
114
- >
115
- }
116
- | {
117
- type: 'global_actions'
118
- actions: Record<string, PluginActionV2 | PluginAction>
119
- }
120
- | { type: 'theme'; theme: Record<string, OldTheme | Theme> }
121
- | { type: 'mode'; mode: 'design' | 'test' }
122
- | { type: 'attrs'; attrs: Record<string, unknown> }
123
- | { type: 'selection'; selectedNodeId: string | null }
124
- | { type: 'highlight'; highlightedNodeId: string | null }
125
- | {
126
- type: 'click' | 'mousemove' | 'dblclick'
127
- metaKey: boolean
128
- x: number
129
- y: number
130
- }
131
- | { type: 'report_document_scroll_size' }
132
- | { type: 'update_inner_text'; innerText: string }
133
- | { type: 'reload' }
134
- | { type: 'fetch_api'; apiKey: string }
135
- | { type: 'introspect_qraphql_api'; apiKey: string }
136
- | { type: 'drag-started'; x: number; y: number }
137
- | { type: 'drag-ended'; canceled?: true }
138
- | { type: 'keydown'; key: string; altKey: boolean; metaKey: boolean }
139
- | { type: 'keyup'; key: string; altKey: boolean; metaKey: boolean }
140
- | {
141
- type: 'get_computed_style'
142
- styles?: string[]
143
- }
144
- | {
145
- type: 'set_timeline_keyframes'
146
- keyframes: Record<string, AnimationKeyframe> | null
147
- }
148
- | {
149
- type: 'set_timeline_time'
150
- time: number | null
151
- timingFunction:
152
- | 'linear'
153
- | 'ease'
154
- | 'ease-in'
155
- | 'ease-out'
156
- | 'ease-in-out'
157
- | 'step-start'
158
- | 'step-end'
159
- | string
160
- | undefined
161
- fillMode: 'none' | 'forwards' | 'backwards' | 'both' | undefined
162
- }
163
- | {
164
- type: 'preview_style'
165
- styles: Record<string, string> | null
166
- theme?: {
167
- key: string
168
- value: Theme
169
- }
170
- }
171
- | {
172
- type: 'preview_theme'
173
- theme: string | null
174
- }
175
-
176
- /**
177
- * Styles required for rendering the same exact text again somewhere else (on a overlay rect in the editor)
178
- */
179
- enum TextNodeComputedStyles {
180
- // Caret color is important as it is the only visible part of the text node (when text is not highlighted)
181
- CARET_COLOR = 'caret-color',
182
- DISPLAY = 'display',
183
- FONT_FAMILY = 'font-family',
184
- FONT_SIZE = 'font-size',
185
- FONT_WEIGHT = 'font-weight',
186
- FONT_STYLE = 'font-style',
187
- FONT_VARIANT = 'font-variant',
188
- FONT_STRETCH = 'font-stretch',
189
- LINE_HEIGHT = 'line-height',
190
- TEXT_ALIGN = 'text-align',
191
- TEXT_TRANSFORM = 'text-transform',
192
- LETTER_SPACING = 'letter-spacing',
193
- WHITE_SPACE = 'white-space',
194
- WORD_SPACING = 'word-spacing',
195
- TEXT_INDENT = 'text-indent',
196
- TEXT_OVERFLOW = 'text-overflow',
197
- TEXT_RENDERING = 'text-rendering',
198
- WORD_BREAK = 'word-break',
199
- WORD_WRAP = 'word-wrap',
200
- DIRECTION = 'direction',
201
- UNICODE_BIDI = 'unicode-bidi',
202
- VERTICAL_ALIGN = 'vertical-align',
203
- FONT_KERNING = 'font-kerning',
204
- FONT_FEATURE_SETTINGS = 'font-feature-settings',
205
- FONT_VARIATION_SETTINGS = 'font-variation-settings',
206
- FONT_SMOOTHING = '-webkit-font-smoothing',
207
- ANTI_ALIASING = '-moz-osx-font-smoothing',
208
- FONT_OPTICAL_SIZING = 'font-optical-sizing',
209
- TAB_SIZE = 'tab-size',
210
- HYPHENS = 'hyphens',
211
- TEXT_ORIENTATION = 'text-orientation',
212
- WRITING_MODE = 'writing-mode',
213
- LINE_BREAK = 'line-break',
214
- OVERFLOW_WRAP = 'overflow-wrap',
215
- }
216
-
217
85
  let env: ToddleEnv
218
86
 
219
87
  export const initGlobalObject = () => {
@@ -312,6 +180,18 @@ export const initGlobalObject = () => {
312
180
  )
313
181
  }
314
182
 
183
+ const EMPTY_COMPONENT_DATA: ComponentData = {
184
+ Location: {
185
+ query: {},
186
+ params: {},
187
+ page: '/',
188
+ path: '/',
189
+ hash: '',
190
+ },
191
+ Attributes: {},
192
+ Variables: {},
193
+ }
194
+
315
195
  // imported by "/.toddle/preview" (see worker/src/preview.ts)
316
196
  export const createRoot = (
317
197
  domNode: HTMLElement | null = document.getElementById('App'),
@@ -319,35 +199,8 @@ export const createRoot = (
319
199
  if (!domNode) {
320
200
  throw new Error('Cant find root domNode')
321
201
  }
322
- const isInputTarget = (event: Event) => {
323
- const target = event.target
324
- if (target instanceof HTMLElement) {
325
- if (
326
- target.tagName === 'INPUT' ||
327
- target.tagName === 'TEXTAREA' ||
328
- target.tagName === 'SELECT' ||
329
- target.tagName === 'STYLE-EDITOR'
330
- ) {
331
- return true
332
- }
333
- if (target.contentEditable?.toLocaleLowerCase() === 'true') {
334
- return true
335
- }
336
- }
337
- return false
338
- }
339
202
 
340
- const dataSignal = signal<ComponentData>({
341
- Location: {
342
- query: {},
343
- params: {},
344
- page: '/',
345
- path: '/',
346
- hash: '',
347
- },
348
- Attributes: {},
349
- Variables: {},
350
- })
203
+ const dataSignal = signal(EMPTY_COMPONENT_DATA)
351
204
  let ctxDataSignal: Signal<ComponentData> | undefined
352
205
 
353
206
  let ctx: ComponentContext | null = null
@@ -387,87 +240,26 @@ export const createRoot = (
387
240
  let previewStyleAnimationFrame = -1
388
241
  let timelineTimeAnimationFrame = -1
389
242
 
390
- /**
391
- * Modifies all link nodes on a component
392
- * NOTE: alters in place
393
- */
394
- const updateComponentLinks = (component: Component) => {
395
- // Find all links and add target="_blank" to them
396
- Object.entries(component.nodes ?? {}).forEach(([_, node]) => {
397
- if (node.type === 'element' && node.tag === 'a') {
398
- node.attrs['target'] = valueFormula('_blank')
399
- }
400
- })
401
- return component
402
- }
403
-
404
- const registerActions = (
405
- allActions: Record<string, PluginActionV2 | PluginAction>,
406
- packageName?: string,
407
- ) => {
408
- const actions: Record<string, PluginActionV2> = {}
409
- Object.entries(allActions ?? {}).forEach(([name, action]) => {
410
- if (typeof action.name === 'string' && action.version === undefined) {
411
- // Legacy actions are self-registering. We need to execute them to register them
412
- Function(action.handler)()
413
- return
414
- }
415
- // We need to convert the handler string into a real function
416
- actions[name] = {
417
- ...(action as PluginActionV2),
418
- handler:
419
- typeof action.handler === 'string'
420
- ? (new Function(
421
- 'args, ctx',
422
- `${action.handler}
423
- return ${safeFunctionName(action.name)}(args, ctx)`,
424
- ) as ActionHandlerV2)
425
- : action.handler,
426
- }
427
- })
428
- window.toddle.actions[packageName ?? window.__toddle.project] = actions
429
- }
430
-
431
- const registerFormulas = (
432
- allFormulas: Record<
433
- string,
434
- ToddleFormula | CodeFormula<FormulaHandlerV2> | CodeFormula<string>
435
- >,
436
- packageName?: string,
437
- ) => {
438
- const formulas: Record<string, PluginFormula<FormulaHandlerV2>> = {}
439
- Object.entries(allFormulas ?? {}).forEach(([name, formula]) => {
440
- if (
441
- !isToddleFormula<FormulaHandlerV2 | string>(formula) &&
442
- typeof formula.name === 'string' &&
443
- formula.version === undefined
444
- ) {
445
- // Legacy formulas are self-registering. We need to execute them to register them
446
- Function(formula.handler as unknown as string)()
447
- return
448
- } else if (!isToddleFormula<FormulaHandlerV2 | string>(formula)) {
449
- // For code formulas we need to convert the handler string into a real function
450
- formulas[name] = {
451
- ...formula,
452
- handler:
453
- typeof formula.handler === 'string'
454
- ? (new Function(
455
- 'args, ctx',
456
- `${formula.handler}
457
- return ${safeFunctionName(formula.name)}(args, ctx)`,
458
- ) as FormulaHandlerV2)
459
- : formula.handler,
243
+ const setupDataSignalSubscribers = () => {
244
+ dataSignal.subscribe((data) => {
245
+ if (component && components && packageComponents && data) {
246
+ try {
247
+ postMessageToEditor({ type: 'data', data })
248
+ } catch {
249
+ // If we're unable to send the data, let's try to JSON serialize it
250
+ postMessageToEditor({
251
+ type: 'data',
252
+ data: JSON.parse(JSON.stringify(data)),
253
+ })
460
254
  }
461
- return
462
255
  }
463
- formulas[name] = formula as PluginFormula<FormulaHandlerV2>
464
256
  })
465
- window.toddle.formulas[packageName ?? window.__toddle.project] = formulas
466
257
  }
258
+ setupDataSignalSubscribers()
467
259
 
468
260
  window.addEventListener(
469
261
  'message',
470
- async (message: MessageEvent<ToddlePreviewEvent>) => {
262
+ async (message: MessageEvent<NordcraftPreviewEvent>) => {
471
263
  if (!message.isTrusted) {
472
264
  console.error('UNTRUSTED MESSAGE')
473
265
  }
@@ -481,11 +273,22 @@ export const createRoot = (
481
273
  | undefined
482
274
 
483
275
  if (message.data.component.name !== component?.name) {
276
+ // Store scroll state for the previous component
484
277
  storeScrollState(component?.name)
278
+ // Remove all subscribers from the previous showSignal
485
279
  showSignal.cleanSubscribers()
280
+ // Clear any previously overridden conditional elements
281
+ showSignal.set({ displayedNodes: [], testMode: mode === 'test' })
282
+ // Restore scroll state for the new component
486
283
  scrollStateRestorer = getScrollStateRestorer(
487
284
  message.data.component.name,
488
285
  )
286
+ // Destroy the dataSignal (including subscribers) for the previous component
287
+ dataSignal.destroy()
288
+ // Re-subscribe all dataSignal subscribers
289
+ setupDataSignalSubscribers()
290
+ // Re-initialize the data signal for the new component
291
+ ctxDataSignal?.destroy()
489
292
  }
490
293
 
491
294
  component = updateComponentLinks(message.data.component)
@@ -550,7 +353,7 @@ export const createRoot = (
550
353
  ctx.components = allComponents
551
354
  }
552
355
 
553
- updateStyle()
356
+ updateStyle(component)
554
357
  update()
555
358
  }
556
359
 
@@ -582,7 +385,7 @@ export const createRoot = (
582
385
  ctx.components = allComponents
583
386
  }
584
387
 
585
- updateStyle()
388
+ updateStyle(component)
586
389
  update()
587
390
  }
588
391
 
@@ -1203,7 +1006,7 @@ export const createRoot = (
1203
1006
  storeScrollState(component?.name)
1204
1007
  })
1205
1008
 
1206
- const updateStyle = () => {
1009
+ const updateStyle = (component: Component | null) => {
1207
1010
  if (component) {
1208
1011
  insertStyles(document.head, component, getAllComponents())
1209
1012
  }
@@ -1574,80 +1377,52 @@ export const createRoot = (
1574
1377
  if (
1575
1378
  fastDeepEqual(newCtx.component.nodes, ctx?.component?.nodes) === false
1576
1379
  ) {
1577
- updateStyle()
1380
+ updateStyle(newCtx.component)
1578
1381
 
1579
1382
  // Remove preview styles automatically when the component changes
1580
1383
  document.head.querySelector('[data-id="selected-node-styles"]')?.remove()
1581
- if (
1582
- fastDeepEqual(
1583
- omitSubnodeStyleForComponent(newCtx.component),
1584
- omitSubnodeStyleForComponent(ctx?.component),
1585
- )
1586
- ) {
1587
- // If we're in here, then the latest update was only a style change, so we should try some optimistic updates
1588
- Object.keys(newCtx.component.nodes).forEach((nodeId) => {
1589
- const newNode = newCtx.component.nodes[nodeId]
1590
- const oldNode = ctx?.component.nodes[nodeId]
1591
- if (
1592
- (newNode.type === 'element' || newNode.type === 'component') &&
1593
- (oldNode?.type === 'element' || oldNode?.type === 'component') &&
1594
- (!fastDeepEqual(newNode.style, oldNode.style) ||
1595
- !fastDeepEqual(newNode.variants, oldNode.variants))
1596
- ) {
1597
- document
1598
- .querySelectorAll(`[data-node-id="${nodeId}"]`)
1599
- .forEach((nodeInstance) => {
1600
- nodeInstance.classList.remove(
1601
- getClassName([oldNode.style, oldNode.variants]),
1602
- )
1603
- nodeInstance.classList.add(
1604
- getClassName([newNode.style, newNode.variants]),
1605
- )
1606
- })
1607
- }
1384
+
1385
+ Array.from(domNode.children).forEach((child) => {
1386
+ if (child.tagName !== 'SCRIPT') {
1387
+ child.remove()
1388
+ }
1389
+ })
1390
+
1391
+ // Clear old root signal and create a new one to not keep old signals with previous root around
1392
+ ctxDataSignal?.destroy()
1393
+ ctxDataSignal = dataSignal.map((data) => data)
1394
+ try {
1395
+ const rootElem = createNode({
1396
+ id: 'root',
1397
+ path: '0',
1398
+ dataSignal: ctxDataSignal,
1399
+ ctx: newCtx,
1400
+ parentElement: domNode,
1401
+ instance: { [newCtx.component.name]: 'root' },
1608
1402
  })
1609
- } else {
1610
- Array.from(domNode.children).forEach((child) => {
1611
- if (child.tagName !== 'SCRIPT') {
1612
- child.remove()
1613
- }
1403
+ newCtx.component.onLoad?.actions.forEach((action) => {
1404
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
1405
+ handleAction(action, dataSignal.get(), newCtx)
1614
1406
  })
1407
+ rootElem.forEach((elem) => domNode.appendChild(elem))
1408
+ } catch (error: unknown) {
1409
+ const isPage = isPageComponent(newCtx.component)
1410
+ let name = `Unexpected error while rendering ${isPage ? 'page' : 'component'}`
1411
+ let message = error instanceof Error ? error.message : String(error)
1412
+ let panic = false
1413
+ if (error instanceof RangeError) {
1414
+ // RangeError is unrecoverable
1415
+ panic = true
1416
+ name = 'Infinite loop detected'
1417
+ message =
1418
+ 'RangeError (Maximum call stack size exceeded): Remove any circular dependencies or recursive calls (Try undoing your last change). This is most likely caused by a component, formula or action using itself.'
1419
+ }
1615
1420
 
1616
- // Clear old root signal and create a new one to not keep old signals with previous root around
1617
- ctxDataSignal?.destroy()
1618
- ctxDataSignal = dataSignal.map((data) => data)
1619
- try {
1620
- const rootElem = createNode({
1621
- id: 'root',
1622
- path: '0',
1623
- dataSignal: ctxDataSignal,
1624
- ctx: newCtx,
1625
- parentElement: domNode,
1626
- instance: { [newCtx.component.name]: 'root' },
1627
- })
1628
- newCtx.component.onLoad?.actions.forEach((action) => {
1629
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
1630
- handleAction(action, dataSignal.get(), newCtx)
1631
- })
1632
- rootElem.forEach((elem) => domNode.appendChild(elem))
1633
- } catch (error: unknown) {
1634
- const isPage = isPageComponent(newCtx.component)
1635
- let name = `Unexpected error while rendering ${isPage ? 'page' : 'component'}`
1636
- let message = error instanceof Error ? error.message : String(error)
1637
- let panic = false
1638
- if (error instanceof RangeError) {
1639
- // RangeError is unrecoverable
1640
- panic = true
1641
- name = 'Infinite loop detected'
1642
- message =
1643
- 'RangeError (Maximum call stack size exceeded): Remove any circular dependencies or recursive calls (Try undoing your last change). This is most likely caused by a component, formula or action using itself.'
1644
- }
1645
-
1646
- // This can be triggered by setting "type" on a select etc.
1647
- if (error instanceof TypeError) {
1648
- panic = true
1649
- name = 'TypeError'
1650
- message = `Type errors are often caused by:
1421
+ // This can be triggered by setting "type" on a select etc.
1422
+ if (error instanceof TypeError) {
1423
+ panic = true
1424
+ name = 'TypeError'
1425
+ message = `Type errors are often caused by:
1651
1426
 
1652
1427
  • Trying to set a read-only property (like "type" on a select element).
1653
1428
 
@@ -1656,36 +1431,35 @@ export const createRoot = (
1656
1431
  • Trying to access a property on an undefined or null value.
1657
1432
 
1658
1433
  • Trying to call a method on an undefined or null value.`
1659
- }
1434
+ }
1660
1435
 
1661
- console.error(name, message, error)
1436
+ console.error(name, message, error)
1662
1437
 
1663
- if (panic) {
1664
- // Show error overlay in the editor until next update
1665
- const panicScreen = createPanicScreen({
1666
- name: name,
1667
- message,
1668
- isPage,
1669
- cause: error,
1670
- })
1438
+ if (panic) {
1439
+ // Show error overlay in the editor until next update
1440
+ const panicScreen = createPanicScreen({
1441
+ name: name,
1442
+ message,
1443
+ isPage,
1444
+ cause: error,
1445
+ })
1671
1446
 
1672
- // Replace the inner HTML of the editor preview with the panic screen
1673
- domNode.innerHTML = ''
1674
- domNode.appendChild(panicScreen)
1675
- } else {
1676
- // Otherwise send a toast to the editor with the error (unknown errors may be recoverable), if not please add the error-type to the above
1677
- sendEditorToast(name, message, {
1678
- type: 'critical',
1679
- })
1680
- }
1447
+ // Replace the inner HTML of the editor preview with the panic screen
1448
+ domNode.innerHTML = ''
1449
+ domNode.appendChild(panicScreen)
1450
+ } else {
1451
+ // Otherwise send a toast to the editor with the error (unknown errors may be recoverable), if not please add the error-type to the above
1452
+ sendEditorToast(name, message, {
1453
+ type: 'critical',
1454
+ })
1681
1455
  }
1682
- postMessageToEditor({
1683
- type: 'style',
1684
- time: new Intl.DateTimeFormat('en-GB', {
1685
- timeStyle: 'long',
1686
- }).format(new Date()),
1687
- })
1688
1456
  }
1457
+ postMessageToEditor({
1458
+ type: 'style',
1459
+ time: new Intl.DateTimeFormat('en-GB', {
1460
+ timeStyle: 'long',
1461
+ }).format(new Date()),
1462
+ })
1689
1463
  }
1690
1464
 
1691
1465
  ctx = newCtx
@@ -1758,68 +1532,7 @@ export const createRoot = (
1758
1532
  return ctx
1759
1533
  }
1760
1534
 
1761
- document.addEventListener('keydown', (event) => {
1762
- if (isInputTarget(event)) {
1763
- return
1764
- }
1765
- switch (event.key) {
1766
- case 'k':
1767
- if (event.metaKey) {
1768
- event.preventDefault()
1769
- }
1770
- }
1771
- postMessageToEditor({
1772
- type: 'keydown',
1773
- event: {
1774
- key: event.key,
1775
- metaKey: event.metaKey,
1776
- shiftKey: event.shiftKey,
1777
- altKey: event.altKey,
1778
- },
1779
- })
1780
- })
1781
- document.addEventListener('keyup', (event) => {
1782
- if (isInputTarget(event)) {
1783
- return
1784
- }
1785
- postMessageToEditor({
1786
- type: 'keyup',
1787
- event: {
1788
- key: event.key,
1789
- metaKey: event.metaKey,
1790
- shiftKey: event.shiftKey,
1791
- altKey: event.altKey,
1792
- },
1793
- })
1794
- })
1795
- document.addEventListener('keypress', (event) => {
1796
- if (isInputTarget(event)) {
1797
- return
1798
- }
1799
- postMessageToEditor({
1800
- type: 'keypress',
1801
- event: {
1802
- key: event.key,
1803
- metaKey: event.metaKey,
1804
- shiftKey: event.shiftKey,
1805
- altKey: event.altKey,
1806
- },
1807
- })
1808
- })
1809
-
1810
- dataSignal.subscribe((data) => {
1811
- if (component && components && packageComponents && data) {
1812
- try {
1813
- postMessageToEditor({ type: 'data', data })
1814
- } catch {
1815
- // If we're unable to send the data, let's try to JSON serialize it
1816
- postMessageToEditor({
1817
- type: 'data',
1818
- data: JSON.parse(JSON.stringify(data)),
1819
- })
1820
- }
1821
- }
1822
- })
1535
+ initKeyListeners()
1823
1536
 
1824
1537
  const clearSelectedStyleVariant = () => {
1825
1538
  if (styleVariantSelection) {
@@ -1880,28 +1593,6 @@ export const createRoot = (
1880
1593
  })()
1881
1594
  }
1882
1595
 
1883
- function getRectData(selectedNode: Element | null | undefined) {
1884
- if (!selectedNode) {
1885
- return null
1886
- }
1887
-
1888
- const { borderRadius, rotate } = window.getComputedStyle(selectedNode)
1889
- const rect: DOMRect = selectedNode.getBoundingClientRect()
1890
-
1891
- return {
1892
- left: rect.left,
1893
- right: rect.right,
1894
- top: rect.top,
1895
- bottom: rect.bottom,
1896
- width: rect.width,
1897
- height: rect.height,
1898
- x: rect.x,
1899
- y: rect.y,
1900
- borderRadius: borderRadius.split(' '),
1901
- rotate,
1902
- }
1903
- }
1904
-
1905
1596
  const insertOrReplaceHeadNode = (id: string, node: Node) => {
1906
1597
  const existing = document.head.querySelector(`[data-meta-id="${id}"]`)
1907
1598
  if (existing) {
@@ -2004,90 +1695,121 @@ const insertTheme = (
2004
1695
  parent.appendChild(styleElem)
2005
1696
  }
2006
1697
 
2007
- type PostMessageType =
2008
- | {
2009
- type: 'textComputedStyle'
2010
- computedStyle: Record<string, string>
2011
- }
2012
- | {
2013
- type: 'selection'
2014
- selectedNodeId: string | null
2015
- }
2016
- | {
2017
- type: 'highlight'
2018
- highlightedNodeId: string | null
2019
- }
2020
- | {
2021
- type: 'navigate'
2022
- name: string
2023
- }
2024
- | {
2025
- type: 'documentScrollSize'
2026
- scrollHeight: number
2027
- scrollWidth: number
2028
- }
2029
- | {
2030
- type: 'nodeMoved'
2031
- copy: boolean
2032
- parent?: string | null
2033
- index?: number
2034
- }
2035
- | {
2036
- type: 'computedStyle'
2037
- computedStyle: Record<string, string>
2038
- }
2039
- | {
2040
- type: 'style'
2041
- time: string
1698
+ const initKeyListeners = () => {
1699
+ document.addEventListener('keydown', (event) => {
1700
+ if (isInputTarget(event)) {
1701
+ return
2042
1702
  }
2043
- | {
2044
- type: 'component event'
2045
- event: any
2046
- time: string
2047
- data: any
1703
+ switch (event.key) {
1704
+ case 'k':
1705
+ if (event.metaKey) {
1706
+ event.preventDefault()
1707
+ }
2048
1708
  }
2049
- | {
2050
- type: 'keydown'
1709
+ postMessageToEditor({
1710
+ type: 'keydown',
2051
1711
  event: {
2052
- key: string
2053
- metaKey: boolean
2054
- shiftKey: boolean
2055
- altKey: boolean
2056
- }
1712
+ key: event.key,
1713
+ metaKey: event.metaKey,
1714
+ shiftKey: event.shiftKey,
1715
+ altKey: event.altKey,
1716
+ },
1717
+ })
1718
+ })
1719
+ document.addEventListener('keyup', (event) => {
1720
+ if (isInputTarget(event)) {
1721
+ return
2057
1722
  }
2058
- | {
2059
- type: 'keyup'
1723
+ postMessageToEditor({
1724
+ type: 'keyup',
2060
1725
  event: {
2061
- key: string
2062
- metaKey: boolean
2063
- shiftKey: boolean
2064
- altKey: boolean
2065
- }
1726
+ key: event.key,
1727
+ metaKey: event.metaKey,
1728
+ shiftKey: event.shiftKey,
1729
+ altKey: event.altKey,
1730
+ },
1731
+ })
1732
+ })
1733
+ document.addEventListener('keypress', (event) => {
1734
+ if (isInputTarget(event)) {
1735
+ return
2066
1736
  }
2067
- | {
2068
- type: 'keypress'
1737
+ postMessageToEditor({
1738
+ type: 'keypress',
2069
1739
  event: {
2070
- key: string
2071
- metaKey: boolean
2072
- shiftKey: boolean
2073
- altKey: boolean
2074
- }
2075
- }
2076
- | { type: 'data'; data: ComponentData }
2077
- | {
2078
- type: 'selectionRect'
2079
- rect: ReturnType<typeof getRectData>
1740
+ key: event.key,
1741
+ metaKey: event.metaKey,
1742
+ shiftKey: event.shiftKey,
1743
+ altKey: event.altKey,
1744
+ },
1745
+ })
1746
+ })
1747
+ }
1748
+
1749
+ const registerActions = (
1750
+ allActions: Record<string, PluginAction>,
1751
+ packageName?: string,
1752
+ ) => {
1753
+ const actions: Record<string, PluginActionV2> = {}
1754
+ Object.entries(allActions ?? {}).forEach(([name, action]) => {
1755
+ if (isLegacyPluginAction(action)) {
1756
+ // Legacy actions are self-registering. We need to execute them to register them
1757
+ Function(action.handler)()
1758
+ return
2080
1759
  }
2081
- | {
2082
- type: 'highlightRect'
2083
- rect: ReturnType<typeof getRectData>
1760
+ // We need to convert the handler string into a real function
1761
+ actions[name] = {
1762
+ ...(action as PluginActionV2),
1763
+ handler:
1764
+ typeof action.handler === 'string'
1765
+ ? (new Function(
1766
+ 'args, ctx',
1767
+ `${action.handler}
1768
+ return ${safeFunctionName(action.name)}(args, ctx)`,
1769
+ ) as ActionHandlerV2)
1770
+ : action.handler,
2084
1771
  }
2085
- | {
2086
- type: 'introspectionResult'
2087
- data: any
2088
- apiKey: string
1772
+ })
1773
+ window.toddle.actions[packageName ?? window.__toddle.project] = actions
1774
+ }
1775
+
1776
+ const registerFormulas = (
1777
+ allFormulas: Record<
1778
+ string,
1779
+ ToddleFormula | CodeFormula<FormulaHandlerV2> | CodeFormula<string>
1780
+ >,
1781
+ packageName?: string,
1782
+ ) => {
1783
+ const formulas: Record<string, PluginFormula<FormulaHandlerV2>> = {}
1784
+ Object.entries(allFormulas ?? {}).forEach(([name, formula]) => {
1785
+ if (
1786
+ !isToddleFormula<FormulaHandlerV2 | string>(formula) &&
1787
+ typeof formula.name === 'string' &&
1788
+ formula.version === undefined
1789
+ ) {
1790
+ // Legacy formulas are self-registering. We need to execute them to register them
1791
+ Function(formula.handler as unknown as string)()
1792
+ return
1793
+ } else if (!isToddleFormula<FormulaHandlerV2 | string>(formula)) {
1794
+ // For code formulas we need to convert the handler string into a real function
1795
+ formulas[name] = {
1796
+ ...formula,
1797
+ handler:
1798
+ typeof formula.handler === 'string'
1799
+ ? (new Function(
1800
+ 'args, ctx',
1801
+ `${formula.handler}
1802
+ return ${safeFunctionName(formula.name)}(args, ctx)`,
1803
+ ) as FormulaHandlerV2)
1804
+ : formula.handler,
1805
+ }
1806
+ return
2089
1807
  }
1808
+ formulas[name] = formula as PluginFormula<FormulaHandlerV2>
1809
+ })
1810
+ window.toddle.formulas[packageName ?? window.__toddle.project] = formulas
1811
+ }
2090
1812
 
2091
- const postMessageToEditor = (message: PostMessageType) => {
1813
+ const postMessageToEditor = (message: EditorPostMessageType) => {
2092
1814
  window.parent?.postMessage(message, '*')
2093
1815
  }