@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,254 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentNodeModel,
|
|
4
|
+
ElementNodeModel,
|
|
5
|
+
NodeStyleModel,
|
|
6
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
7
|
+
import {
|
|
8
|
+
getClassName,
|
|
9
|
+
toValidClassName,
|
|
10
|
+
} from '@nordcraft/core/dist/styling/className'
|
|
11
|
+
import { kebabCase } from '@nordcraft/core/dist/styling/style.css'
|
|
12
|
+
import { variantSelector } from '@nordcraft/core/dist/styling/variantSelector'
|
|
13
|
+
import { omitKeys } from '@nordcraft/core/dist/utils/collections'
|
|
14
|
+
|
|
15
|
+
const LEGACY_BREAKPOINTS = {
|
|
16
|
+
large: 1440,
|
|
17
|
+
small: 576,
|
|
18
|
+
medium: 960,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const SIZE_PROPERTIES = new Set([
|
|
22
|
+
'width',
|
|
23
|
+
'min-width',
|
|
24
|
+
'max-width',
|
|
25
|
+
'height',
|
|
26
|
+
'min-height',
|
|
27
|
+
'max-height',
|
|
28
|
+
'margin',
|
|
29
|
+
'margin-top',
|
|
30
|
+
'margin-left',
|
|
31
|
+
'margin-bottom',
|
|
32
|
+
'margin-right',
|
|
33
|
+
'padding',
|
|
34
|
+
'padding-top',
|
|
35
|
+
'padding-left',
|
|
36
|
+
'padding-bottom',
|
|
37
|
+
'padding-right',
|
|
38
|
+
'gap',
|
|
39
|
+
'gap-x',
|
|
40
|
+
'gap-y',
|
|
41
|
+
'border-radius',
|
|
42
|
+
'border-bottom-left-radius',
|
|
43
|
+
'border-bottom-right-radius',
|
|
44
|
+
'border-top-left-radius',
|
|
45
|
+
'border-top-right-radius',
|
|
46
|
+
'border-width',
|
|
47
|
+
'border-top-width',
|
|
48
|
+
'border-left-width',
|
|
49
|
+
'border-bottom-width',
|
|
50
|
+
'border-right-width',
|
|
51
|
+
'font-size',
|
|
52
|
+
'left',
|
|
53
|
+
'right',
|
|
54
|
+
'top',
|
|
55
|
+
'bottom',
|
|
56
|
+
'outline-width',
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
export const insertStyles = (
|
|
60
|
+
parent: HTMLElement,
|
|
61
|
+
root: Component,
|
|
62
|
+
components: Component[],
|
|
63
|
+
) => {
|
|
64
|
+
const getNodeStyles = (
|
|
65
|
+
node: ElementNodeModel | ComponentNodeModel,
|
|
66
|
+
classHash: string,
|
|
67
|
+
) => {
|
|
68
|
+
const style = omitKeys(node.style ?? {}, [
|
|
69
|
+
'variants',
|
|
70
|
+
'breakpoints',
|
|
71
|
+
'mediaQuery',
|
|
72
|
+
'shadows',
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
const styleElem = document.createElement('style')
|
|
76
|
+
styleElem.setAttribute('data-hash', classHash)
|
|
77
|
+
styleElem.appendChild(
|
|
78
|
+
document.createTextNode(`
|
|
79
|
+
${renderVariant('.' + classHash, style)}
|
|
80
|
+
|
|
81
|
+
${
|
|
82
|
+
node.variants
|
|
83
|
+
? node.variants
|
|
84
|
+
.map((variant) => {
|
|
85
|
+
const selector = `.${classHash}${variantSelector(variant)}`
|
|
86
|
+
const renderedVariant = renderVariant(selector, variant.style, {
|
|
87
|
+
startingStyle: variant.startingStyle,
|
|
88
|
+
})
|
|
89
|
+
return variant.mediaQuery
|
|
90
|
+
? `
|
|
91
|
+
@media (${Object.entries(variant.mediaQuery)
|
|
92
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(') and (')}) {
|
|
95
|
+
${renderedVariant}
|
|
96
|
+
}
|
|
97
|
+
`
|
|
98
|
+
: variant.breakpoint
|
|
99
|
+
? `
|
|
100
|
+
@media (min-width: ${
|
|
101
|
+
LEGACY_BREAKPOINTS[variant.breakpoint]
|
|
102
|
+
}px) {
|
|
103
|
+
${renderedVariant}
|
|
104
|
+
}
|
|
105
|
+
`
|
|
106
|
+
: renderedVariant
|
|
107
|
+
})
|
|
108
|
+
.join('\n')
|
|
109
|
+
: ''
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
${
|
|
113
|
+
node.animations
|
|
114
|
+
? Object.entries(node.animations)
|
|
115
|
+
.map(([animationName, keyframes]) => {
|
|
116
|
+
return `
|
|
117
|
+
@keyframes ${animationName} {
|
|
118
|
+
${Object.values(keyframes)
|
|
119
|
+
.sort((a, b) => a.position - b.position)
|
|
120
|
+
.map(({ key, position, value }) => {
|
|
121
|
+
return `
|
|
122
|
+
${position * 100}% {
|
|
123
|
+
${key}: ${value};
|
|
124
|
+
}
|
|
125
|
+
`
|
|
126
|
+
})
|
|
127
|
+
.join('\n')}
|
|
128
|
+
}
|
|
129
|
+
`
|
|
130
|
+
})
|
|
131
|
+
.join('\n')
|
|
132
|
+
: ''
|
|
133
|
+
}
|
|
134
|
+
`),
|
|
135
|
+
)
|
|
136
|
+
return styleElem
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Make sure that CSS for dependant components are rendered first so that instance styles will override.
|
|
140
|
+
const visitedComponents = new Set<string>()
|
|
141
|
+
const newStyles = new Map<string, Element>()
|
|
142
|
+
function insertComponentStyles(
|
|
143
|
+
component: Component,
|
|
144
|
+
package_name: string | undefined,
|
|
145
|
+
): string | undefined {
|
|
146
|
+
if (visitedComponents.has(component.name)) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
visitedComponents.add(component.name)
|
|
150
|
+
if (!component.nodes) {
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.warn('Unable to find nodes for component', component.name)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
Object.entries(component.nodes).forEach(([id, node]) => {
|
|
156
|
+
if (node.type === 'component') {
|
|
157
|
+
const childComponent = components.find(
|
|
158
|
+
(c) =>
|
|
159
|
+
c.name ===
|
|
160
|
+
[node.package ?? package_name, node.name]
|
|
161
|
+
?.filter((n) => n)
|
|
162
|
+
.join('/'),
|
|
163
|
+
)
|
|
164
|
+
if (childComponent) {
|
|
165
|
+
insertComponentStyles(childComponent, node.package ?? package_name)
|
|
166
|
+
|
|
167
|
+
const instanceClassHash = toValidClassName(
|
|
168
|
+
`${component.name}:${id}`,
|
|
169
|
+
true,
|
|
170
|
+
)
|
|
171
|
+
newStyles.set(
|
|
172
|
+
instanceClassHash,
|
|
173
|
+
getNodeStyles(node, instanceClassHash),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
} else if (node.type === 'element') {
|
|
177
|
+
const classHash = getClassName([node.style, node.variants])
|
|
178
|
+
newStyles.set(classHash, getNodeStyles(node, classHash))
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
insertComponentStyles(root, undefined)
|
|
184
|
+
|
|
185
|
+
// Remove old styles.
|
|
186
|
+
// We do not keep track of changes, so must remove all and re-add as order matters.
|
|
187
|
+
parent.querySelectorAll('[data-hash]').forEach((node) => node.remove())
|
|
188
|
+
|
|
189
|
+
// Add new styles
|
|
190
|
+
const fragment = document.createDocumentFragment()
|
|
191
|
+
newStyles.forEach((style) => {
|
|
192
|
+
fragment.appendChild(style)
|
|
193
|
+
})
|
|
194
|
+
parent.appendChild(fragment)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const renderVariant = (
|
|
198
|
+
selector: string,
|
|
199
|
+
style: NodeStyleModel,
|
|
200
|
+
options?: {
|
|
201
|
+
startingStyle?: boolean
|
|
202
|
+
},
|
|
203
|
+
) => {
|
|
204
|
+
const scrollbarStyles = Object.entries(style).filter(
|
|
205
|
+
([key]) => key === 'scrollbar-width',
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
let styles = styleToCss(style)
|
|
209
|
+
if (options?.startingStyle) {
|
|
210
|
+
styles = `@starting-style {
|
|
211
|
+
${styles}
|
|
212
|
+
}`
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return `
|
|
216
|
+
${selector} {
|
|
217
|
+
${styles}
|
|
218
|
+
}
|
|
219
|
+
${
|
|
220
|
+
scrollbarStyles.length > 0
|
|
221
|
+
? `
|
|
222
|
+
${selector}::-webkit-scrollbar {
|
|
223
|
+
${scrollbarStyles
|
|
224
|
+
.map(([_, value]) => {
|
|
225
|
+
switch (value) {
|
|
226
|
+
case 'none':
|
|
227
|
+
return 'width: 0;'
|
|
228
|
+
case 'thin':
|
|
229
|
+
return 'width:4px;'
|
|
230
|
+
default:
|
|
231
|
+
return ''
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.join('\n')}
|
|
235
|
+
}
|
|
236
|
+
`
|
|
237
|
+
: ''
|
|
238
|
+
}
|
|
239
|
+
`
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const styleToCss = (style: NodeStyleModel) => {
|
|
243
|
+
return Object.entries(style)
|
|
244
|
+
.map(([property, value]) => {
|
|
245
|
+
const propertyName = kebabCase(property)
|
|
246
|
+
const propertyValue =
|
|
247
|
+
String(Number(value)) === String(value) &&
|
|
248
|
+
SIZE_PROPERTIES.has(propertyName)
|
|
249
|
+
? `${Number(value) * 4}px`
|
|
250
|
+
: value
|
|
251
|
+
return `${propertyName}:${propertyValue};`
|
|
252
|
+
})
|
|
253
|
+
.join('\n')
|
|
254
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentData,
|
|
4
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
5
|
+
import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
|
|
6
|
+
import type {
|
|
7
|
+
Toddle as NewToddle,
|
|
8
|
+
Toddle,
|
|
9
|
+
ToddleInternals,
|
|
10
|
+
} from '@nordcraft/core/dist/types'
|
|
11
|
+
import type { Signal } from './signal/signal'
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
__components: Record<string, Signal<ComponentData>> // used for debugging
|
|
16
|
+
__toddle: ToddleInternals
|
|
17
|
+
toddle: NewToddle<LocationSignal, PreviewShowSignal>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LocationSignal = Signal<Location>
|
|
22
|
+
|
|
23
|
+
export interface Location {
|
|
24
|
+
route: Component['route']
|
|
25
|
+
page?: string
|
|
26
|
+
path: string
|
|
27
|
+
params: Record<string, string | null>
|
|
28
|
+
query: Record<string, string | string[] | null>
|
|
29
|
+
hash?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type PreviewShowSignal = Signal<{
|
|
33
|
+
displayedNodes: string[]
|
|
34
|
+
testMode: boolean
|
|
35
|
+
}>
|
|
36
|
+
|
|
37
|
+
interface ListItem {
|
|
38
|
+
Item: unknown
|
|
39
|
+
Index: number
|
|
40
|
+
Parent?: ListItem
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ComponentChild {
|
|
44
|
+
dataSignal: Signal<ComponentData>
|
|
45
|
+
id: string
|
|
46
|
+
path: string
|
|
47
|
+
ctx: ComponentContext
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ComponentContext {
|
|
51
|
+
component: Component
|
|
52
|
+
components: Component[]
|
|
53
|
+
package: string | undefined
|
|
54
|
+
abortSignal: AbortSignal
|
|
55
|
+
root: Document | ShadowRoot
|
|
56
|
+
isRootComponent: boolean
|
|
57
|
+
dataSignal: Signal<ComponentData>
|
|
58
|
+
triggerEvent: (event: string, data: unknown) => void
|
|
59
|
+
apis: Record<string, ContextApi>
|
|
60
|
+
children: Record<string, Array<ComponentChild>>
|
|
61
|
+
formulaCache: Record<
|
|
62
|
+
string,
|
|
63
|
+
{
|
|
64
|
+
get: (data: ComponentData) => { hit: true; data: any } | { hit: false }
|
|
65
|
+
set: (data: ComponentData, result: any) => void
|
|
66
|
+
}
|
|
67
|
+
>
|
|
68
|
+
providers: Record<
|
|
69
|
+
string,
|
|
70
|
+
{
|
|
71
|
+
component: Component
|
|
72
|
+
formulaDataSignals: Record<string, Signal<ComponentData>>
|
|
73
|
+
ctx: ComponentContext
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
toddle: Toddle<LocationSignal, PreviewShowSignal>
|
|
77
|
+
env: ToddleEnv
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ContextApi = {
|
|
81
|
+
fetch: Function
|
|
82
|
+
destroy: Function
|
|
83
|
+
update?: Function // for updating the dataSignal (v2 only)
|
|
84
|
+
triggerActions?: Function // for triggering actions explicitly. Useful when initializing apis (v2 only)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type FormulaCache = Record<
|
|
88
|
+
string,
|
|
89
|
+
{
|
|
90
|
+
get: (data: ComponentData) => { hit: true; data: any } | { hit: false }
|
|
91
|
+
set: (data: ComponentData, result: any) => void
|
|
92
|
+
}
|
|
93
|
+
>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A helper class to batch multiple callbacks and process them in a single update step just before the next frame render, but after the current stack.
|
|
3
|
+
* This is more efficient than processing each callback in a separate requestAnimationFrame due to the overhead.
|
|
4
|
+
*/
|
|
5
|
+
export class BatchQueue {
|
|
6
|
+
private batchQueue: Array<() => void> = []
|
|
7
|
+
private isProcessing = false
|
|
8
|
+
private processBatch() {
|
|
9
|
+
if (this.isProcessing) return
|
|
10
|
+
this.isProcessing = true
|
|
11
|
+
|
|
12
|
+
requestAnimationFrame(() => {
|
|
13
|
+
while (this.batchQueue.length > 0) {
|
|
14
|
+
const callback = this.batchQueue.shift()
|
|
15
|
+
callback?.()
|
|
16
|
+
}
|
|
17
|
+
this.isProcessing = false
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
public add(callback: () => void) {
|
|
21
|
+
this.batchQueue.push(callback)
|
|
22
|
+
this.processBatch()
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Component,
|
|
3
|
+
ComponentData,
|
|
4
|
+
} from '@nordcraft/core/dist/component/component.types'
|
|
5
|
+
import type {
|
|
6
|
+
Formula,
|
|
7
|
+
FunctionOperation,
|
|
8
|
+
} from '@nordcraft/core/dist/formula/formula'
|
|
9
|
+
import { get, mapObject } from '@nordcraft/core/dist/utils/collections'
|
|
10
|
+
import { isDefined } from '@nordcraft/core/dist/utils/util'
|
|
11
|
+
import type { FormulaCache } from '../types'
|
|
12
|
+
|
|
13
|
+
export function createFormulaCache(component: Component): FormulaCache {
|
|
14
|
+
if (!isDefined(component.formulas)) {
|
|
15
|
+
return {}
|
|
16
|
+
}
|
|
17
|
+
return mapObject(component.formulas, ([name, f]) => {
|
|
18
|
+
const { canCache, keys } = f.memoize
|
|
19
|
+
? getFormulaCacheConfig(f.formula, component)
|
|
20
|
+
: { canCache: false, keys: [] }
|
|
21
|
+
let cacheInput: any
|
|
22
|
+
let cacheData: any
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
name,
|
|
26
|
+
{
|
|
27
|
+
get: (data: ComponentData) => {
|
|
28
|
+
if (
|
|
29
|
+
canCache &&
|
|
30
|
+
cacheInput &&
|
|
31
|
+
keys.every((key) => {
|
|
32
|
+
return get(data, key) === get(cacheInput, key)
|
|
33
|
+
})
|
|
34
|
+
) {
|
|
35
|
+
return { hit: true, data: cacheData }
|
|
36
|
+
}
|
|
37
|
+
return { hit: false }
|
|
38
|
+
},
|
|
39
|
+
set: (data: ComponentData, result: any) => {
|
|
40
|
+
if (canCache) {
|
|
41
|
+
cacheInput = data
|
|
42
|
+
cacheData = result
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
]
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getFormulaCacheConfig(formula: Formula, component: Component) {
|
|
51
|
+
const paths: string[][] = []
|
|
52
|
+
function visitOperation(op: Formula) {
|
|
53
|
+
if (!op) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
if (op.type == 'path' && op.path[0] !== 'Args') {
|
|
57
|
+
paths.push(op.path)
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray((op as any)?.arguments)) {
|
|
60
|
+
;(op as FunctionOperation)?.arguments.forEach((arg) =>
|
|
61
|
+
visitOperation(arg.formula),
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
if (op.type === 'record' && Array.isArray(op.entries)) {
|
|
65
|
+
op.entries.forEach((arg) => visitOperation(arg.formula))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (op.type === 'apply') {
|
|
69
|
+
if (!component.formulas?.[op.name]?.memoize) {
|
|
70
|
+
throw new Error('Cannot memoize')
|
|
71
|
+
}
|
|
72
|
+
visitOperation(component.formulas?.[op.name]?.formula)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
visitOperation(formula)
|
|
77
|
+
} catch {
|
|
78
|
+
return {
|
|
79
|
+
canCache: false,
|
|
80
|
+
keys: [],
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const keys: string[][] = []
|
|
85
|
+
paths
|
|
86
|
+
.sort((a, b) => a.length - b.length)
|
|
87
|
+
.forEach((path) => {
|
|
88
|
+
if (!keys.some((key) => key.every((k, i) => k === path[i]))) {
|
|
89
|
+
keys.push(path)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
return {
|
|
93
|
+
canCache: true,
|
|
94
|
+
keys,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Line, Point } from '../editor/types'
|
|
2
|
+
import { findNearestLine } from './findNearestLine'
|
|
3
|
+
|
|
4
|
+
describe('findNearestLine', () => {
|
|
5
|
+
test('should find the nearest line to a point', () => {
|
|
6
|
+
const lines: Line[] = [
|
|
7
|
+
{ x1: 0, y1: 0, x2: 0, y2: 1 },
|
|
8
|
+
{ x1: 1, y1: 0, x2: 1, y2: 1 },
|
|
9
|
+
{ x1: 2, y1: 0, x2: 2, y2: 1 },
|
|
10
|
+
]
|
|
11
|
+
const point: Point = { x: 1.5, y: 0.5 }
|
|
12
|
+
const nearestLine = findNearestLine(lines, point)?.nearestLine
|
|
13
|
+
expect(nearestLine).toEqual({ x1: 1, y1: 0, x2: 1, y2: 1 })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('should handle point exactly on a line', () => {
|
|
17
|
+
const lines: Line[] = [
|
|
18
|
+
{ x1: 0, y1: 0, x2: 2, y2: 0 },
|
|
19
|
+
{ x1: 1, y1: -1, x2: 1, y2: 1 },
|
|
20
|
+
]
|
|
21
|
+
const point: Point = { x: 1, y: 0.5 }
|
|
22
|
+
const nearestLine = findNearestLine(lines, point)?.nearestLine
|
|
23
|
+
expect(nearestLine).toEqual({ x1: 1, y1: -1, x2: 1, y2: 1 })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('should handle empty lines array', () => {
|
|
27
|
+
const lines: Line[] = []
|
|
28
|
+
const point: Point = { x: 0, y: 0 }
|
|
29
|
+
expect(findNearestLine(lines, point).nearestLine).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('should handle lines with zero length (points)', () => {
|
|
33
|
+
const lines: Line[] = [
|
|
34
|
+
{ x1: 1, y1: 1, x2: 1, y2: 1 },
|
|
35
|
+
{ x1: 2, y1: 2, x2: 3, y2: 3 },
|
|
36
|
+
]
|
|
37
|
+
const point: Point = { x: 1, y: 1.5 }
|
|
38
|
+
const nearestLine = findNearestLine(lines, point)?.nearestLine
|
|
39
|
+
expect(nearestLine).toEqual({ x1: 1, y1: 1, x2: 1, y2: 1 })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('should handle negative coordinates', () => {
|
|
43
|
+
const lines: Line[] = [
|
|
44
|
+
{ x1: -2, y1: -2, x2: -2, y2: 2 },
|
|
45
|
+
{ x1: -2, y1: 2, x2: 2, y2: 2 },
|
|
46
|
+
{ x1: 2, y1: 2, x2: 2, y2: -2 },
|
|
47
|
+
{ x1: 2, y1: -2, x2: -2, y2: -2 },
|
|
48
|
+
]
|
|
49
|
+
const point: Point = { x: 0, y: 0 }
|
|
50
|
+
const nearestLine = findNearestLine(lines, point)?.nearestLine
|
|
51
|
+
// The point is inside the square; function should return one of the sides
|
|
52
|
+
expect(nearestLine).toEqual({ x1: -2, y1: -2, x2: -2, y2: 2 })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('should handle when multiple lines are equally close', () => {
|
|
56
|
+
const lines: Line[] = [
|
|
57
|
+
{ x1: 0, y1: 1, x2: 1, y2: 1 },
|
|
58
|
+
{ x1: 0, y1: -1, x2: 1, y2: -1 },
|
|
59
|
+
]
|
|
60
|
+
const point: Point = { x: 0.5, y: 0 }
|
|
61
|
+
const nearestLine = findNearestLine(lines, point)?.nearestLine
|
|
62
|
+
// Both lines are at distance 1; the function should return the first one
|
|
63
|
+
expect(nearestLine).toEqual({ x1: 0, y1: 1, x2: 1, y2: 1 })
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Line, Point } from '../editor/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Finds the nearest line to a given point from an array of lines.
|
|
5
|
+
*
|
|
6
|
+
* @param lines - An array of line segments defined by their endpoints.
|
|
7
|
+
* @param point - The point to which the nearest line is to be found.
|
|
8
|
+
* @returns The line segment nearest to the given point.
|
|
9
|
+
*/
|
|
10
|
+
export function findNearestLine(
|
|
11
|
+
lines: Line[],
|
|
12
|
+
point: Point,
|
|
13
|
+
): { nearestLine: Line | null; dist: number; projectionPoint: number } {
|
|
14
|
+
let minDistSquared = Infinity
|
|
15
|
+
let nearestLine: Line | null = null
|
|
16
|
+
let nearestProjectionPoint = 0
|
|
17
|
+
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const { distSquared, projectionPoint } = distancePointToSegmentSquared(
|
|
20
|
+
point,
|
|
21
|
+
line,
|
|
22
|
+
)
|
|
23
|
+
if (distSquared < minDistSquared) {
|
|
24
|
+
minDistSquared = distSquared
|
|
25
|
+
nearestLine = line
|
|
26
|
+
nearestProjectionPoint = projectionPoint
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
nearestLine,
|
|
32
|
+
dist: Math.sqrt(minDistSquared),
|
|
33
|
+
projectionPoint: nearestProjectionPoint,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Represents where the perpendicular projection of the point onto the infinite line lies relative to the line segment:
|
|
39
|
+
* - If t is 0, the projection is at (x1, y1).
|
|
40
|
+
* - If t is 1, the projection is at (x2, y2).
|
|
41
|
+
* - If t is between 0 and 1, the projection lies somewhere between the two endpoints.
|
|
42
|
+
*
|
|
43
|
+
* (x1, y1) *---o-----------* (x2, y2)
|
|
44
|
+
* | /
|
|
45
|
+
* | /
|
|
46
|
+
* | /
|
|
47
|
+
* | /
|
|
48
|
+
* | /
|
|
49
|
+
* MIN_DIST -> | /
|
|
50
|
+
* | /
|
|
51
|
+
* | /
|
|
52
|
+
* | /
|
|
53
|
+
* | /
|
|
54
|
+
* |/
|
|
55
|
+
* (x, y)
|
|
56
|
+
*
|
|
57
|
+
* Where "o" The length along the line segment where the projection point lies.
|
|
58
|
+
*
|
|
59
|
+
* Notice that the calculation works for diagonal lines as well as horizontal and vertical lines.
|
|
60
|
+
*/
|
|
61
|
+
function distancePointToSegmentSquared(point: Point, line: Line) {
|
|
62
|
+
const dx = line.x2 - line.x1
|
|
63
|
+
const dy = line.y2 - line.y1
|
|
64
|
+
const l2 = dx * dx + dy * dy
|
|
65
|
+
|
|
66
|
+
// If l2 is zero, the line segment is actually a point. We return the squared distance between the point and this single point.
|
|
67
|
+
if (l2 === 0) {
|
|
68
|
+
return {
|
|
69
|
+
distSquared:
|
|
70
|
+
(point.x - line.x1) * (point.x - line.x1) +
|
|
71
|
+
(point.y - line.y1) * (point.y - line.y1),
|
|
72
|
+
projectionPoint: 0.5,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let projectionPoint =
|
|
77
|
+
((point.x - line.x1) * dx + (point.y - line.y1) * dy) / l2
|
|
78
|
+
|
|
79
|
+
// Clamp t to [0,1] for the projection point to lie within the finite line segment.
|
|
80
|
+
projectionPoint = Math.max(0, Math.min(1, projectionPoint))
|
|
81
|
+
|
|
82
|
+
const projX = line.x1 + projectionPoint * dx
|
|
83
|
+
const projY = line.y1 + projectionPoint * dy
|
|
84
|
+
|
|
85
|
+
// No need to take the square root, since we are only comparing distances and square root is monotonic.
|
|
86
|
+
return {
|
|
87
|
+
distSquared:
|
|
88
|
+
(point.x - projX) * (point.x - projX) +
|
|
89
|
+
(point.y - projY) * (point.y - projY),
|
|
90
|
+
projectionPoint,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function getDragData(event: Event) {
|
|
2
|
+
if (event instanceof DragEvent) {
|
|
3
|
+
return Array.from(event.dataTransfer?.items ?? []).reduce<
|
|
4
|
+
Record<string, any>
|
|
5
|
+
>((dragData, item) => {
|
|
6
|
+
dragData[item.type] = event.dataTransfer?.getData(item.type)
|
|
7
|
+
return dragData
|
|
8
|
+
}, {})
|
|
9
|
+
}
|
|
10
|
+
return
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ElementNodeModel } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
import type { ComponentContext } from '../types'
|
|
3
|
+
|
|
4
|
+
export function getElementTagName(
|
|
5
|
+
node: ElementNodeModel,
|
|
6
|
+
ctx: ComponentContext,
|
|
7
|
+
id: string,
|
|
8
|
+
) {
|
|
9
|
+
if (ctx.component.version === 2 && id === 'root') {
|
|
10
|
+
return `${ctx.package ?? ctx.toddle.project}-${node.tag}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return node.tag
|
|
14
|
+
}
|