@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,381 @@
1
+ /* eslint-disable no-console */
2
+ import type {
3
+ ComponentData,
4
+ NodeModel,
5
+ SupportedNamespaces,
6
+ } from '@nordcraft/core/dist/component/component.types'
7
+ import { applyFormula } from '@nordcraft/core/dist/formula/formula'
8
+ import { toBoolean } from '@nordcraft/core/dist/utils/util'
9
+ import type { Signal } from '../signal/signal'
10
+ import { signal } from '../signal/signal'
11
+ import type { ComponentContext } from '../types'
12
+ import { ensureEfficientOrdering, getNextSiblingElement } from '../utils/nodes'
13
+ import { createComponent } from './createComponent'
14
+ import { createElement } from './createElement'
15
+ import { createSlot } from './createSlot'
16
+ import { createText } from './createText'
17
+
18
+ export function createNode({
19
+ id,
20
+ dataSignal,
21
+ path,
22
+ ctx,
23
+ namespace,
24
+ parentElement,
25
+ instance,
26
+ }: {
27
+ id: string
28
+ dataSignal: Signal<ComponentData>
29
+ path: string
30
+ ctx: ComponentContext
31
+ namespace?: SupportedNamespaces
32
+ parentElement: Element | ShadowRoot
33
+ instance: Record<string, string>
34
+ }): ReadonlyArray<Element | Text> {
35
+ const node = ctx.component.nodes[id]
36
+ if (!node) {
37
+ return []
38
+ }
39
+ const create = ({
40
+ node,
41
+ ...props
42
+ }: NodeRenderer<NodeModel>): ReadonlyArray<Element | Text> => {
43
+ switch (node.type) {
44
+ case 'element':
45
+ return [
46
+ createElement({
47
+ node,
48
+ ...props,
49
+ }),
50
+ ]
51
+ case 'component':
52
+ // eslint-disable-next-line no-case-declarations
53
+ const isLocalComponent = ctx.components.some(
54
+ (c) => c.name === node.name,
55
+ )
56
+ return createComponent({
57
+ node: { ...node, id }, // we need the node id for instance classes
58
+ ...props,
59
+ ctx: {
60
+ ...ctx,
61
+ package:
62
+ node.package ?? (isLocalComponent ? undefined : ctx.package),
63
+ },
64
+ parentElement,
65
+ })
66
+ case 'text':
67
+ return [createText({ ...props, node })]
68
+ case 'slot':
69
+ return createSlot({ ...props, node })
70
+ }
71
+ }
72
+
73
+ function conditional({
74
+ node,
75
+ dataSignal,
76
+ id,
77
+ path,
78
+ ctx,
79
+ namespace,
80
+ parentElement,
81
+ instance,
82
+ }: NodeRenderer<NodeModel>): ReadonlyArray<Element | Text> {
83
+ let firstRun = true
84
+ let childDataSignal: Signal<ComponentData> | null = null
85
+ const showSignal = dataSignal.map((data) =>
86
+ toBoolean(
87
+ applyFormula(node.condition, {
88
+ data,
89
+ component: ctx.component,
90
+ formulaCache: ctx.formulaCache,
91
+ root: ctx.root,
92
+ package: ctx.package,
93
+ toddle: ctx.toddle,
94
+ env: ctx.env,
95
+ }),
96
+ ),
97
+ )
98
+
99
+ const elements: Array<Element | Text> = []
100
+ const toggle = (show: boolean) => {
101
+ if (show && elements.length === 0) {
102
+ childDataSignal?.destroy()
103
+ childDataSignal = dataSignal.map((data) => data)
104
+ elements.push(
105
+ ...create({
106
+ node,
107
+ dataSignal: childDataSignal,
108
+ path,
109
+ id,
110
+ ctx,
111
+ namespace,
112
+ parentElement,
113
+ instance,
114
+ }),
115
+ )
116
+
117
+ // No reason to continue if we are on first run, as the render phase has not yet been reached
118
+ if (firstRun) {
119
+ return
120
+ }
121
+
122
+ if (!parentElement || ctx.root.contains(parentElement) === false) {
123
+ console.error(
124
+ `Conditional: Parent element does not exist for "${path}" This is likely due to the DOM being modified outside of toddle.`,
125
+ )
126
+ return
127
+ }
128
+
129
+ if (parentElement.querySelector(`[data-id="${path}"]`)) {
130
+ console.warn(
131
+ `Conditional: Element with data-id="${path}" already exists. This is likely due to the DOM being modified outside of toddle`,
132
+ )
133
+ return
134
+ }
135
+
136
+ const nextPathElement = getNextSiblingElement(path, parentElement)
137
+ for (const element of elements) {
138
+ parentElement.insertBefore(element, nextPathElement)
139
+ }
140
+ } else if (!show) {
141
+ childDataSignal?.destroy()
142
+ elements.forEach((elem) => elem.remove())
143
+ elements.splice(0, elements.length)
144
+ }
145
+ }
146
+
147
+ showSignal.subscribe(toggle, {
148
+ destroy: () => {
149
+ childDataSignal?.destroy()
150
+ },
151
+ })
152
+ if (ctx.env.runtime === 'preview' && ctx.toddle._preview) {
153
+ ctx.toddle._preview.showSignal.subscribe(
154
+ ({ displayedNodes, testMode }) => {
155
+ if (displayedNodes.includes(path) && !testMode) {
156
+ // only override the default show if we are in design mode (not test mode)
157
+ toggle(true)
158
+ } else {
159
+ toggle(showSignal.get())
160
+ }
161
+ },
162
+ )
163
+ }
164
+
165
+ firstRun = false
166
+ return elements
167
+ }
168
+
169
+ function repeat(): ReadonlyArray<Element | Text> {
170
+ let firstRun = true
171
+ let repeatItems = new Map<
172
+ string | number,
173
+ {
174
+ dataSignal: Signal<ComponentData>
175
+ cleanup: () => void
176
+ elements: ReadonlyArray<Element | Text>
177
+ }
178
+ >()
179
+ const repeatSignal = dataSignal.map((data) => {
180
+ const list = applyFormula(node.repeat, {
181
+ data,
182
+ component: ctx.component,
183
+ formulaCache: ctx.formulaCache,
184
+ root: ctx.root,
185
+ package: ctx.package,
186
+ toddle: ctx.toddle,
187
+ env: ctx.env,
188
+ })
189
+ if (typeof list !== 'object') {
190
+ return []
191
+ }
192
+ return Object.entries(list ?? {})
193
+ })
194
+
195
+ repeatSignal.subscribe(
196
+ (list) => {
197
+ const newRepeatItems = new Map<
198
+ string | number,
199
+ {
200
+ dataSignal: Signal<ComponentData>
201
+ cleanup: () => void
202
+ elements: ReadonlyArray<Element | Text>
203
+ }
204
+ >()
205
+
206
+ for (let i = 0; i < list.length; i++) {
207
+ const [Key, Item] = list[i]
208
+ const childData = {
209
+ ...dataSignal.get(),
210
+ ListItem: {
211
+ ...(dataSignal.get().ListItem
212
+ ? { Parent: dataSignal.get().ListItem }
213
+ : {}),
214
+ Item,
215
+ Index: Number(i),
216
+ Key,
217
+ },
218
+ }
219
+
220
+ let childKey = node.repeatKey
221
+ ? applyFormula(node.repeatKey, {
222
+ data: childData,
223
+ component: ctx.component,
224
+ formulaCache: ctx.formulaCache,
225
+ root: ctx.root,
226
+ package: ctx.package,
227
+ toddle: ctx.toddle,
228
+ env: ctx.env,
229
+ })
230
+ : Key
231
+
232
+ // Can't we just use the Item reference as key as we have fine-grained reactivity at this point?
233
+ // That way we won't need repeatKey at all as everything will be optimized by reference?!?
234
+ // https://github.com/solidjs/solid/discussions/366#discussioncomment-1220239
235
+ // childKey = Item
236
+ // Do fallback to Key(index) if repeatKey has duplicate values.
237
+ // This will essentially disable the optimization for repeatKey and will always re-render the children on every change.
238
+ if (newRepeatItems.has(childKey)) {
239
+ console.warn(
240
+ `Duplicate key "${childKey}" found in repeat. Fallback to index as key. This will cause a re-render of the duplicated children on every change.`,
241
+ )
242
+ childKey = Key
243
+ }
244
+
245
+ const existingItem = repeatItems.get(childKey)
246
+ if (existingItem) {
247
+ newRepeatItems.set(childKey, existingItem)
248
+ existingItem.dataSignal.update((data) => {
249
+ return {
250
+ ...data,
251
+ ListItem: {
252
+ ...(dataSignal.get().ListItem
253
+ ? { Parent: dataSignal.get().ListItem }
254
+ : {}),
255
+ Item,
256
+ Index: Number(i),
257
+ Key,
258
+ },
259
+ }
260
+ })
261
+ } else {
262
+ const childDataSignal = signal<ComponentData>(childData)
263
+ const cleanup = dataSignal.subscribe(
264
+ (data) => {
265
+ if (firstRun) {
266
+ return
267
+ }
268
+
269
+ childDataSignal.update(({ ListItem }) => {
270
+ return {
271
+ ...data,
272
+ ListItem,
273
+ }
274
+ })
275
+ },
276
+ {
277
+ destroy: () => childDataSignal.destroy(),
278
+ },
279
+ )
280
+
281
+ const args = {
282
+ node,
283
+ id,
284
+ dataSignal: childDataSignal,
285
+ path: Key === '0' ? path : `${path}(${Key})`,
286
+ ctx,
287
+ namespace,
288
+ parentElement,
289
+ instance,
290
+ }
291
+ const elements = node.condition ? conditional(args) : create(args)
292
+ newRepeatItems.set(childKey, {
293
+ dataSignal: childDataSignal,
294
+ cleanup,
295
+ elements,
296
+ })
297
+ }
298
+ }
299
+
300
+ // Cleanup removed items' data
301
+ Array.from(repeatItems.entries()).forEach(([key, item]) => {
302
+ if (!newRepeatItems.has(key)) {
303
+ item.cleanup()
304
+ item.dataSignal.destroy()
305
+ item.elements.forEach((e) => e.remove())
306
+ }
307
+ })
308
+ repeatItems = newRepeatItems
309
+
310
+ // No reason to continue if we are on first run, as the render-phase for the parent
311
+ // has not yet been reached, or if there are no items to render
312
+ if (firstRun || repeatItems.size === 0) {
313
+ return
314
+ }
315
+
316
+ if (!parentElement || ctx.root.contains(parentElement) === false) {
317
+ console.error(
318
+ `Repeat: Parent element does not exist for ${path}. This is likely due to the DOM being modified outside of toddle.`,
319
+ )
320
+ return
321
+ }
322
+
323
+ ensureEfficientOrdering(
324
+ parentElement,
325
+ Array.from(repeatItems.values()).flatMap((e) => e.elements),
326
+ getNextSiblingElement(path, parentElement),
327
+ )
328
+ },
329
+ {
330
+ destroy: () =>
331
+ Array.from(repeatItems.values()).forEach((e) => {
332
+ e.cleanup()
333
+ e.dataSignal.destroy()
334
+ e.elements.forEach((e) => e.remove())
335
+ }),
336
+ },
337
+ )
338
+
339
+ // We utilize that the signal subscription runs synchronously above,
340
+ // so we already have a populated repeatItems map to return initially.
341
+ // Note: `repeatItems.values()` is okay here, as maps' iterator is ordered by insertion.
342
+ firstRun = false
343
+ return Array.from(repeatItems.values()).flatMap((e) => e.elements)
344
+ }
345
+
346
+ if (node.repeat) {
347
+ return repeat()
348
+ }
349
+ if (node.condition) {
350
+ return conditional({
351
+ node,
352
+ dataSignal,
353
+ ctx,
354
+ id,
355
+ path,
356
+ namespace,
357
+ parentElement,
358
+ instance,
359
+ })
360
+ }
361
+ return create({
362
+ node,
363
+ dataSignal,
364
+ ctx,
365
+ id,
366
+ path,
367
+ namespace,
368
+ parentElement,
369
+ instance,
370
+ })
371
+ }
372
+ export type NodeRenderer<NodeType> = {
373
+ node: NodeType
374
+ dataSignal: Signal<ComponentData>
375
+ id: string
376
+ path: string
377
+ ctx: ComponentContext
378
+ namespace?: SupportedNamespaces
379
+ parentElement: Element | ShadowRoot
380
+ instance: Record<string, string>
381
+ }
@@ -0,0 +1,61 @@
1
+ import type { SlotNodeModel } from '@nordcraft/core/dist/component/component.types'
2
+ import type { NodeRenderer } from './createNode'
3
+ import { createNode } from './createNode'
4
+
5
+ export function createSlot({
6
+ path,
7
+ node,
8
+ dataSignal,
9
+ ctx,
10
+ parentElement,
11
+ instance,
12
+ namespace,
13
+ }: NodeRenderer<SlotNodeModel>): ReadonlyArray<Element | Text> {
14
+ const slotName = node.name ?? 'default'
15
+ let children: Array<Element | Text> = []
16
+ // Is slotted content provided?
17
+ if (ctx.children[slotName]) {
18
+ children = ctx.children[slotName].flatMap((child) => {
19
+ const childDataSignal = child.dataSignal.map((data) => data)
20
+ dataSignal.subscribe((data) => data, {
21
+ destroy: () => childDataSignal.destroy(),
22
+ })
23
+ return createNode({
24
+ ...child,
25
+ dataSignal: childDataSignal,
26
+ parentElement,
27
+ ctx: {
28
+ ...child.ctx,
29
+ providers: ctx.providers,
30
+ },
31
+ instance,
32
+ namespace,
33
+ })
34
+ })
35
+ } else {
36
+ // Otherwise, return placeholder content
37
+ children = node.children.flatMap((child, i) => {
38
+ return createNode({
39
+ id: child,
40
+ path: path + '.' + i,
41
+ dataSignal,
42
+ ctx,
43
+ parentElement,
44
+ instance,
45
+ namespace,
46
+ })
47
+ })
48
+ }
49
+
50
+ if (ctx.env.runtime === 'custom-element' && ctx.isRootComponent) {
51
+ const webComponentSlot = document.createElement('slot')
52
+ webComponentSlot.setAttribute('name', slotName)
53
+ children.forEach((child) => {
54
+ webComponentSlot.appendChild(child)
55
+ })
56
+
57
+ return [webComponentSlot]
58
+ }
59
+
60
+ return children
61
+ }
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from '@jest/globals'
2
+ import type { ComponentData } from '@nordcraft/core/dist/component/component.types'
3
+ import { valueFormula } from '@nordcraft/core/dist/formula/formulaUtils'
4
+ import { Signal } from '../signal/signal'
5
+ import type { ComponentContext } from '../types'
6
+ import { createText } from './createText'
7
+
8
+ describe('createText()', () => {
9
+ test('it returns a span element with text in it while in default namespace', () => {
10
+ let textElement = createText({
11
+ ctx: {
12
+ isRootComponent: false,
13
+ component: { name: 'My Component' },
14
+ } as Partial<ComponentContext> as any,
15
+ namespace: 'http://www.w3.org/1999/xhtml',
16
+ dataSignal: undefined as any,
17
+ path: 'test-text-element',
18
+ id: 'test-text-element-id',
19
+ node: {
20
+ type: 'text',
21
+ value: valueFormula('Hello world'),
22
+ },
23
+ })
24
+ expect(textElement instanceof HTMLSpanElement).toBe(true)
25
+ textElement = textElement as HTMLSpanElement
26
+ expect(textElement.tagName).toBe('SPAN')
27
+ expect(textElement.getAttribute('data-node-id')).toBe(
28
+ 'test-text-element-id',
29
+ )
30
+ expect(textElement.getAttribute('data-id')).toBe('test-text-element')
31
+ expect(textElement.getAttribute('data-component')).toBe('My Component')
32
+ expect(textElement.children.length).toBe(0)
33
+ expect(textElement.innerText).toBe('Hello world')
34
+ })
35
+ test('it returns a text node while not in the default namespace', () => {
36
+ const textElement = createText({
37
+ ctx: {
38
+ isRootComponent: false,
39
+ component: { name: 'My Component' },
40
+ } as Partial<ComponentContext> as any,
41
+ namespace: 'http://www.w3.org/2000/svg',
42
+ dataSignal: undefined as any,
43
+ path: 'test-text-element',
44
+ id: 'test-text-element-id',
45
+ node: {
46
+ type: 'text',
47
+ value: valueFormula('Hello world'),
48
+ },
49
+ }) as Text
50
+ expect(textElement instanceof Text).toBe(true)
51
+ expect(textElement.textContent).toBe('Hello world')
52
+ })
53
+ test('it does not add a data-component attribute for root elements', () => {
54
+ const textElement = createText({
55
+ ctx: {
56
+ isRootComponent: true,
57
+ } as Partial<ComponentContext> as any,
58
+ dataSignal: undefined as any,
59
+ path: 'test-text-element',
60
+ id: 'test-text-element-id',
61
+ node: {
62
+ type: 'text',
63
+ value: valueFormula('Hello world'),
64
+ },
65
+ }) as HTMLSpanElement
66
+ expect(textElement.getAttribute('data-component')).toBeNull()
67
+ })
68
+ test('Signal changes update the text element', () => {
69
+ const dataSignal = new Signal<ComponentData>({
70
+ Attributes: { text: 'Hello world' },
71
+ })
72
+ const textElement = createText({
73
+ ctx: { dataSignal } as Partial<ComponentContext> as any,
74
+ dataSignal,
75
+ path: '',
76
+ id: '',
77
+ node: {
78
+ type: 'text',
79
+ value: {
80
+ type: 'path',
81
+ path: ['Attributes', 'text'],
82
+ },
83
+ },
84
+ })
85
+ expect(textElement.textContent).toBe('Hello world')
86
+ dataSignal.set({ Attributes: { text: 'Goodbye world' } })
87
+ expect(textElement.textContent).toBe('Goodbye world')
88
+ })
89
+ test('Show formulas are not respected for text elements', () => {
90
+ const textElement = createText({
91
+ ctx: {} as Partial<ComponentContext> as any,
92
+ dataSignal: undefined as any,
93
+ path: '',
94
+ id: '',
95
+ node: {
96
+ type: 'text',
97
+ value: valueFormula('Hello world'),
98
+ condition: valueFormula(false),
99
+ },
100
+ })
101
+ expect(textElement.textContent).toBe('Hello world')
102
+ })
103
+ test('Repeat formulas are not respected for text elements', () => {
104
+ const textElement = createText({
105
+ ctx: {} as Partial<ComponentContext> as any,
106
+ dataSignal: undefined as any,
107
+ path: '',
108
+ id: '',
109
+ node: {
110
+ type: 'text',
111
+ value: valueFormula('Hello world'),
112
+ repeat: valueFormula(['1', '2', '3']),
113
+ },
114
+ })
115
+ expect(textElement.textContent).toBe('Hello world')
116
+ })
117
+ })
@@ -0,0 +1,104 @@
1
+ import type {
2
+ ComponentData,
3
+ SupportedNamespaces,
4
+ TextNodeModel,
5
+ } from '@nordcraft/core/dist/component/component.types'
6
+ import { applyFormula } from '@nordcraft/core/dist/formula/formula'
7
+ import type { Signal } from '../signal/signal'
8
+ import type { ComponentContext } from '../types'
9
+
10
+ export type RenderTextProps = {
11
+ node: TextNodeModel
12
+ dataSignal: Signal<ComponentData>
13
+ id: string
14
+ path: string
15
+ namespace?: SupportedNamespaces
16
+ ctx: ComponentContext
17
+ }
18
+
19
+ /**
20
+ * Create a text node
21
+ *
22
+ * Note: We wrap the text in a <span> to make it easier to select/highlight the text node in the preview.
23
+ * We should find a better way to do this without wrapping the node, and instead use `createTextNode`.
24
+ */
25
+ export function createText({
26
+ node,
27
+ id,
28
+ path,
29
+ dataSignal,
30
+ namespace,
31
+ ctx,
32
+ }: RenderTextProps): HTMLSpanElement | Text {
33
+ // Span element is not valid outside of the default namespace
34
+ if (namespace && namespace !== 'http://www.w3.org/1999/xhtml') {
35
+ return createTextNS({ node, dataSignal, ctx })
36
+ }
37
+
38
+ const { value } = node
39
+ const elem = document.createElement('span')
40
+ elem.setAttribute('data-node-id', id)
41
+ if (typeof id === 'string') {
42
+ elem.setAttribute('data-id', path)
43
+ }
44
+ if (ctx.isRootComponent === false) {
45
+ elem.setAttribute('data-component', ctx.component.name)
46
+ }
47
+ elem.setAttribute('data-node-type', 'text')
48
+ if (value.type !== 'value') {
49
+ const sig = dataSignal.map((data) =>
50
+ String(
51
+ applyFormula(value, {
52
+ data,
53
+ component: ctx.component,
54
+ formulaCache: ctx.formulaCache,
55
+ root: ctx.root,
56
+ package: ctx.package,
57
+ toddle: ctx.toddle,
58
+ env: ctx.env,
59
+ }),
60
+ ),
61
+ )
62
+ sig.subscribe((value) => {
63
+ elem.innerText = value
64
+ })
65
+ } else {
66
+ elem.innerText = String(value.value)
67
+ }
68
+ return elem
69
+ }
70
+
71
+ /**
72
+ * This function is technically more performant than `createText` because it doesn't create a wrapping <span> element.
73
+ * We would like to use this everywhere eventually, but we need to handle raw text selection in the editor (possibly by utilizing text ranges).
74
+ */
75
+ export function createTextNS({
76
+ node,
77
+ dataSignal,
78
+ ctx,
79
+ }: Pick<RenderTextProps, 'node' | 'dataSignal' | 'ctx'>): Text {
80
+ const { value } = node
81
+ const textNode = document.createTextNode('')
82
+ if (value.type !== 'value') {
83
+ const sig = dataSignal.map((data) =>
84
+ String(
85
+ applyFormula(value, {
86
+ data,
87
+ component: ctx.component,
88
+ formulaCache: ctx.formulaCache,
89
+ root: ctx.root,
90
+ package: ctx.package,
91
+ toddle: ctx.toddle,
92
+ env: ctx.env,
93
+ }),
94
+ ),
95
+ )
96
+ sig.subscribe((value) => {
97
+ textNode.nodeValue = value
98
+ })
99
+ } else {
100
+ textNode.nodeValue = String(value.value)
101
+ }
102
+
103
+ return textNode
104
+ }