@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.
- package/README.md +5 -0
- package/dist/api/createAPI.d.ts +20 -0
- package/dist/api/createAPI.js +319 -0
- package/dist/api/createAPI.js.map +1 -0
- package/dist/api/createAPIv2.d.ts +7 -0
- package/dist/api/createAPIv2.js +686 -0
- package/dist/api/createAPIv2.js.map +1 -0
- package/dist/components/createComponent.d.ts +13 -0
- package/dist/components/createComponent.js +216 -0
- package/dist/components/createComponent.js.map +1 -0
- package/dist/components/createElement.d.ts +3 -0
- package/dist/components/createElement.js +208 -0
- package/dist/components/createElement.js.map +1 -0
- package/dist/components/createNode.d.ts +22 -0
- package/dist/components/createNode.js +272 -0
- package/dist/components/createNode.js.map +1 -0
- package/dist/components/createSlot.d.ts +3 -0
- package/dist/components/createSlot.js +49 -0
- package/dist/components/createSlot.js.map +1 -0
- package/dist/components/createText.d.ts +23 -0
- package/dist/components/createText.js +68 -0
- package/dist/components/createText.js.map +1 -0
- package/dist/components/createText.test.d.ts +1 -0
- package/dist/components/createText.test.js +113 -0
- package/dist/components/createText.test.js.map +1 -0
- package/dist/components/renderComponent.d.ts +34 -0
- package/dist/components/renderComponent.js +66 -0
- package/dist/components/renderComponent.js.map +1 -0
- package/dist/context/isContextProvider.d.ts +2 -0
- package/dist/context/isContextProvider.js +5 -0
- package/dist/context/isContextProvider.js.map +1 -0
- package/dist/context/subscribeToContext.d.ts +4 -0
- package/dist/context/subscribeToContext.js +93 -0
- package/dist/context/subscribeToContext.js.map +1 -0
- package/dist/custom-components/components.d.ts +1 -0
- package/dist/custom-components/components.js +2 -0
- package/dist/custom-components/components.js.map +1 -0
- package/dist/custom-components/toddle-portal.d.ts +6 -0
- package/dist/custom-components/toddle-portal.js +20 -0
- package/dist/custom-components/toddle-portal.js.map +1 -0
- package/dist/custom-element/ToddleComponent.d.ts +37 -0
- package/dist/custom-element/ToddleComponent.js +244 -0
- package/dist/custom-element/ToddleComponent.js.map +1 -0
- package/dist/custom-element/defineComponents.d.ts +26 -0
- package/dist/custom-element/defineComponents.js +42 -0
- package/dist/custom-element/defineComponents.js.map +1 -0
- package/dist/custom-element.main.d.ts +3 -0
- package/dist/custom-element.main.esm.js +266 -0
- package/dist/custom-element.main.esm.js.map +7 -0
- package/dist/custom-element.main.js +14 -0
- package/dist/custom-element.main.js.map +1 -0
- package/dist/debug/logState.d.ts +4 -0
- package/dist/debug/logState.js +19 -0
- package/dist/debug/logState.js.map +1 -0
- package/dist/editor/drag-drop/dragEnded.d.ts +2 -0
- package/dist/editor/drag-drop/dragEnded.js +56 -0
- package/dist/editor/drag-drop/dragEnded.js.map +1 -0
- package/dist/editor/drag-drop/dragMove.d.ts +3 -0
- package/dist/editor/drag-drop/dragMove.js +74 -0
- package/dist/editor/drag-drop/dragMove.js.map +1 -0
- package/dist/editor/drag-drop/dragReorder.d.ts +3 -0
- package/dist/editor/drag-drop/dragReorder.js +92 -0
- package/dist/editor/drag-drop/dragReorder.js.map +1 -0
- package/dist/editor/drag-drop/dragStarted.d.ts +9 -0
- package/dist/editor/drag-drop/dragStarted.js +100 -0
- package/dist/editor/drag-drop/dragStarted.js.map +1 -0
- package/dist/editor/drag-drop/dropHighlight.d.ts +16 -0
- package/dist/editor/drag-drop/dropHighlight.js +50 -0
- package/dist/editor/drag-drop/dropHighlight.js.map +1 -0
- package/dist/editor/drag-drop/getInsertAreas.d.ts +20 -0
- package/dist/editor/drag-drop/getInsertAreas.js +220 -0
- package/dist/editor/drag-drop/getInsertAreas.js.map +1 -0
- package/dist/editor-preview.main.d.ts +19 -0
- package/dist/editor-preview.main.js +1303 -0
- package/dist/editor-preview.main.js.map +1 -0
- package/dist/events/handleAction.d.ts +3 -0
- package/dist/events/handleAction.js +307 -0
- package/dist/events/handleAction.js.map +1 -0
- package/dist/page.main.d.ts +7 -0
- package/dist/page.main.esm.js +8 -0
- package/dist/page.main.esm.js.map +7 -0
- package/dist/page.main.js +395 -0
- package/dist/page.main.js.map +1 -0
- package/dist/signal/signal.d.ts +19 -0
- package/dist/signal/signal.js +65 -0
- package/dist/signal/signal.js.map +1 -0
- package/dist/styles/style.d.ts +4 -0
- package/dist/styles/style.js +196 -0
- package/dist/styles/style.js.map +1 -0
- package/dist/utils/BatchQueue.d.ts +10 -0
- package/dist/utils/BatchQueue.js +25 -0
- package/dist/utils/BatchQueue.js.map +1 -0
- package/dist/utils/createFormulaCache.d.ts +3 -0
- package/dist/utils/createFormulaCache.js +81 -0
- package/dist/utils/createFormulaCache.js.map +1 -0
- package/dist/utils/findNearestLine.d.ts +13 -0
- package/dist/utils/findNearestLine.js +74 -0
- package/dist/utils/findNearestLine.js.map +1 -0
- package/dist/utils/findNearestLine.test.d.ts +1 -0
- package/dist/utils/findNearestLine.test.js +59 -0
- package/dist/utils/findNearestLine.test.js.map +1 -0
- package/dist/utils/getDragData.d.ts +1 -0
- package/dist/utils/getDragData.js +10 -0
- package/dist/utils/getDragData.js.map +1 -0
- package/dist/utils/getElementTagName.d.ts +3 -0
- package/dist/utils/getElementTagName.js +7 -0
- package/dist/utils/getElementTagName.js.map +1 -0
- package/dist/utils/nodes.d.ts +21 -0
- package/dist/utils/nodes.js +89 -0
- package/dist/utils/nodes.js.map +1 -0
- package/dist/utils/omitStyle.d.ts +2 -0
- package/dist/utils/omitStyle.js +13 -0
- package/dist/utils/omitStyle.js.map +1 -0
- package/dist/utils/rectHasPoint.d.ts +2 -0
- package/dist/utils/rectHasPoint.js +4 -0
- package/dist/utils/rectHasPoint.js.map +1 -0
- package/dist/utils/setAttribute.d.ts +4 -0
- package/dist/utils/setAttribute.js +57 -0
- package/dist/utils/setAttribute.js.map +1 -0
- package/dist/utils/tryStartViewTransition.d.ts +5 -0
- package/dist/utils/tryStartViewTransition.js +14 -0
- package/dist/utils/tryStartViewTransition.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.js +36 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +25 -0
- package/src/api/createAPI.ts +375 -0
- package/src/api/createAPIv2.ts +931 -0
- package/src/components/createComponent.ts +280 -0
- package/src/components/createElement.ts +240 -0
- package/src/components/createNode.ts +381 -0
- package/src/components/createSlot.ts +61 -0
- package/src/components/createText.test.ts +117 -0
- package/src/components/createText.ts +104 -0
- package/src/components/renderComponent.ts +145 -0
- package/src/context/isContextProvider.ts +12 -0
- package/src/context/subscribeToContext.ts +135 -0
- package/src/custom-components/components.ts +1 -0
- package/src/custom-components/toddle-portal.ts +19 -0
- package/src/custom-element/ToddleComponent.ts +315 -0
- package/src/custom-element/defineComponents.ts +65 -0
- package/src/custom-element.main.ts +24 -0
- package/src/debug/logState.ts +30 -0
- package/src/editor/drag-drop/dragEnded.ts +75 -0
- package/src/editor/drag-drop/dragMove.ts +95 -0
- package/src/editor/drag-drop/dragReorder.ts +137 -0
- package/src/editor/drag-drop/dragStarted.ts +145 -0
- package/src/editor/drag-drop/dropHighlight.ts +82 -0
- package/src/editor/drag-drop/getInsertAreas.ts +235 -0
- package/src/editor/types.d.ts +36 -0
- package/src/editor-preview.main.ts +1782 -0
- package/src/events/handleAction.ts +387 -0
- package/src/page.main.ts +489 -0
- package/src/signal/signal.ts +74 -0
- package/src/styles/style.ts +254 -0
- package/src/types.d.ts +93 -0
- package/src/utils/BatchQueue.ts +24 -0
- package/src/utils/createFormulaCache.ts +96 -0
- package/src/utils/findNearestLine.test.ts +65 -0
- package/src/utils/findNearestLine.ts +92 -0
- package/src/utils/getDragData.ts +11 -0
- package/src/utils/getElementTagName.ts +14 -0
- package/src/utils/nodes.ts +125 -0
- package/src/utils/omitStyle.ts +19 -0
- package/src/utils/rectHasPoint.ts +5 -0
- package/src/utils/setAttribute.ts +56 -0
- package/src/utils/tryStartViewTransition.ts +32 -0
- 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
|
+
}
|