flet-box 0.0.0-stage → 1.0.2
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/LICENSE +21 -0
- package/README.md +190 -2
- package/bin/cli.js +394 -0
- package/bin/commands/buildAndroid.js +200 -0
- package/bin/commands/buildIOS.js +237 -0
- package/bin/commands/create.js +406 -0
- package/bin/commands/createBundle.js +375 -0
- package/bin/commands/createComponent.js +115 -0
- package/bin/commands/createScreen.js +161 -0
- package/bin/commands/killServer.js +107 -0
- package/bin/commands/package.js +131 -0
- package/bin/commands/release.js +189 -0
- package/bin/commands/runBundle.js +62 -0
- package/bin/commands/runCloudflare.js +89 -0
- package/bin/commands/runNative.js +109 -0
- package/bin/commands/runServer.js +390 -0
- package/bin/ota/loader.js +361 -0
- package/bin/utils/bundleConfig.js +178 -0
- package/bin/utils/capacitor.js +137 -0
- package/bin/utils/colors.js +215 -0
- package/bin/utils/helpers.js +80 -0
- package/bin/utils/spinner.js +96 -0
- package/bin/utils/templates.js +1824 -0
- package/bin/utils/version.js +108 -0
- package/package.json +58 -4
- package/src/animations/AnimatedBox.js +607 -0
- package/src/animations/AnimatedText.js +105 -0
- package/src/animations/MatrixRain.js +220 -0
- package/src/animations/ParallaxBox.js +369 -0
- package/src/animations/animate.js +332 -0
- package/src/animations/index.js +25 -0
- package/src/app.js +41 -0
- package/src/assets/icon-1024-maskable.png +0 -0
- package/src/assets/icon-192-maskable.png +0 -0
- package/src/assets/icon-192.png +0 -0
- package/src/assets/icon-512-maskable.png +0 -0
- package/src/assets/icon-512.png +0 -0
- package/src/assets/logo.png +0 -0
- package/src/core/App.js +44 -0
- package/src/core/hmr-client.js +142 -0
- package/src/core/index.js +34 -0
- package/src/core/pwa.js +100 -0
- package/src/core/runApp.js +430 -0
- package/src/create-index.sh +237 -0
- package/src/fonts/MaterialIcons-Regular.woff2 +0 -0
- package/src/fonts/icons.css +23 -0
- package/src/index.d.ts +1930 -0
- package/src/index.js +288 -0
- package/src/navigations/AdaptiveScaffold.js +149 -0
- package/src/navigations/AppBar.js +331 -0
- package/src/navigations/BottomNavigation.js +298 -0
- package/src/navigations/CollapsibleSideBar.js +139 -0
- package/src/navigations/Drawer.js +333 -0
- package/src/navigations/DrawerItem.js +260 -0
- package/src/navigations/Router.js +475 -0
- package/src/navigations/Scaffold.js +537 -0
- package/src/navigations/Tabs.js +384 -0
- package/src/navigations/index.js +57 -0
- package/src/services/RamStore.js +213 -0
- package/src/services/Session.js +310 -0
- package/src/services/Storage.js +309 -0
- package/src/services/http.js +350 -0
- package/src/services/index.js +57 -0
- package/src/tools/animation.js +42 -0
- package/src/tools/array.js +95 -0
- package/src/tools/border.js +28 -0
- package/src/tools/clipboard.js +104 -0
- package/src/tools/color.js +329 -0
- package/src/tools/createList.js +215 -0
- package/src/tools/delay.js +55 -0
- package/src/tools/device.js +138 -0
- package/src/tools/dict.js +297 -0
- package/src/tools/dimensions.js +118 -0
- package/src/tools/filter.js +41 -0
- package/src/tools/flex.js +26 -0
- package/src/tools/gradient.js +34 -0
- package/src/tools/grid.js +57 -0
- package/src/tools/index.js +38 -0
- package/src/tools/mapList.js +107 -0
- package/src/tools/margin.js +93 -0
- package/src/tools/memo.js +60 -0
- package/src/tools/os.js +104 -0
- package/src/tools/padding.js +93 -0
- package/src/tools/print.js +69 -0
- package/src/tools/random.js +299 -0
- package/src/tools/ref.js +70 -0
- package/src/tools/rgba.js +4 -0
- package/src/tools/shadow.js +23 -0
- package/src/tools/string.js +85 -0
- package/src/tools/time.js +674 -0
- package/src/tools/transform.js +68 -0
- package/src/tools/transition.js +48 -0
- package/src/tools/useState.js +268 -0
- package/src/tools/uuid.js +42 -0
- package/src/utils/TextInputValidator.js +256 -0
- package/src/utils/composeUpdate.js +65 -0
- package/src/utils/getWidgetProps.js +35 -0
- package/src/utils/index.js +76 -0
- package/src/utils/markdownParser.js +368 -0
- package/src/utils/mediaTime.js +79 -0
- package/src/utils/navigation.js +263 -0
- package/src/utils/scriptInjector.js +73 -0
- package/src/utils/stackPosition.js +31 -0
- package/src/utils/stopWebRefresh.js +77 -0
- package/src/utils/styleInjector.js +38 -0
- package/src/utils/syntaxHighlight.js +339 -0
- package/src/utils/themes.js +347 -0
- package/src/utils/units.js +87 -0
- package/src/utils/visualEffects.js +169 -0
- package/src/widget-factory/addChildren.js +114 -0
- package/src/widget-factory/assignProps.js +256 -0
- package/src/widget-factory/createWidget.js +28 -0
- package/src/widget-factory/effects.js +132 -0
- package/src/widget-factory/index.js +7 -0
- package/src/widget-factory/lifecycle.js +210 -0
- package/src/widget-factory/makeParentable.js +89 -0
- package/src/widget-factory/pixTool.js +40 -0
- package/src/widget-factory/processProps.js +122 -0
- package/src/widget-factory/reactivity.js +98 -0
- package/src/widget-factory/remTool.js +34 -0
- package/src/widget-factory/stackPosition.js +87 -0
- package/src/widget-factory/stringTool.js +26 -0
- package/src/widget-factory/tools.js +249 -0
- package/src/widget-factory/translateProps.js +1049 -0
- package/src/widget-factory/warnings.js +284 -0
- package/src/widget-factory/widgetFactory.js +200 -0
- package/src/widgets/Accordion.js +562 -0
- package/src/widgets/AdMob.js +315 -0
- package/src/widgets/AdSense.js +268 -0
- package/src/widgets/AlertDialog.js +162 -0
- package/src/widgets/Audio.js +311 -0
- package/src/widgets/Avatar.js +210 -0
- package/src/widgets/Badge.js +189 -0
- package/src/widgets/BottomSheet.js +287 -0
- package/src/widgets/Button.js +247 -0
- package/src/widgets/Card.js +33 -0
- package/src/widgets/Carousel.js +301 -0
- package/src/widgets/Chart.js +632 -0
- package/src/widgets/Checkbox.js +106 -0
- package/src/widgets/Chip.js +153 -0
- package/src/widgets/CircularBar.js +352 -0
- package/src/widgets/CircularChart.js +549 -0
- package/src/widgets/CodeViewer.js +236 -0
- package/src/widgets/Column.js +29 -0
- package/src/widgets/Container.js +31 -0
- package/src/widgets/DataTable.js +257 -0
- package/src/widgets/Divider.js +93 -0
- package/src/widgets/DraggBox.js +115 -0
- package/src/widgets/Dropdown.js +505 -0
- package/src/widgets/DroppBox.js +180 -0
- package/src/widgets/FloatingActionButton.js +91 -0
- package/src/widgets/GridView.js +20 -0
- package/src/widgets/Icon.js +33 -0
- package/src/widgets/Image.js +15 -0
- package/src/widgets/Input.js +565 -0
- package/src/widgets/Inspector.js +254 -0
- package/src/widgets/InstallButton.js +110 -0
- package/src/widgets/ListTile.js +224 -0
- package/src/widgets/ListView.js +503 -0
- package/src/widgets/Markdown.js +1236 -0
- package/src/widgets/Modal.js +496 -0
- package/src/widgets/Pagination.js +195 -0
- package/src/widgets/ProgressBar.js +178 -0
- package/src/widgets/QRCode.js +467 -0
- package/src/widgets/Radio.js +132 -0
- package/src/widgets/Rating.js +261 -0
- package/src/widgets/Row.js +29 -0
- package/src/widgets/Skeleton.js +243 -0
- package/src/widgets/Slider.js +527 -0
- package/src/widgets/SnackBar.js +227 -0
- package/src/widgets/Stack.js +29 -0
- package/src/widgets/Stepper.js +296 -0
- package/src/widgets/Switch.js +102 -0
- package/src/widgets/Text.js +98 -0
- package/src/widgets/Tooltip.js +267 -0
- package/src/widgets/TreeView.js +370 -0
- package/src/widgets/Video.js +92 -0
- package/src/widgets/index.js +52 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/modules/FletBox/utils/units.js
|
|
2
|
+
// import { toREM, setBaseFontSize, toPX } from './utils/units.js';
|
|
3
|
+
//
|
|
4
|
+
// // Global configuration (optional)
|
|
5
|
+
// setBaseFontSize(16); // 1rem = 16px (default)
|
|
6
|
+
//
|
|
7
|
+
// // Automatic conversions
|
|
8
|
+
// toREM(16) // "1rem"
|
|
9
|
+
// toREM(24) // "1.5rem"
|
|
10
|
+
// toREM(8) // "0.5rem"
|
|
11
|
+
//
|
|
12
|
+
// // Strings pass through as-is
|
|
13
|
+
// toREM('16px') // "16px"
|
|
14
|
+
// toREM('1rem') // "1rem"
|
|
15
|
+
// toREM('50%') // "50%"
|
|
16
|
+
//
|
|
17
|
+
// // For exact pixels
|
|
18
|
+
// toPX(16) // "16px"
|
|
19
|
+
// toPX(24) // "24px"
|
|
20
|
+
// toPX('1rem') // "1rem"
|
|
21
|
+
|
|
22
|
+
let BASE_FONT_SIZE = 16;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sets the base font size for rem conversion
|
|
26
|
+
* @param {number} size - Base size in pixels (default 16)
|
|
27
|
+
* @example
|
|
28
|
+
* setBaseFontSize(16); // 1rem = 16px
|
|
29
|
+
* setBaseFontSize(20); // 1rem = 20px
|
|
30
|
+
*/
|
|
31
|
+
export const setBaseFontSize = (size) => {
|
|
32
|
+
BASE_FONT_SIZE = size;
|
|
33
|
+
document.documentElement.style.fontSize = `${size}px`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts a numeric value to rem, or keeps strings as they are
|
|
38
|
+
* @param {number|string} value - Value to convert
|
|
39
|
+
* @returns {string} Converted value (e.g. "1rem", "16px", "50%")
|
|
40
|
+
* @example
|
|
41
|
+
* toREM(16) // "1rem"
|
|
42
|
+
* toREM(8) // "0.5rem"
|
|
43
|
+
* toREM('16px') // "16px"
|
|
44
|
+
* toREM('1rem') // "1rem"
|
|
45
|
+
* toREM('50%') // "50%"
|
|
46
|
+
*/
|
|
47
|
+
export const toREM = (value) => {
|
|
48
|
+
if (value === undefined || value === null) return undefined;
|
|
49
|
+
|
|
50
|
+
// If it is a number, convert to rem
|
|
51
|
+
if (typeof value === "number") {
|
|
52
|
+
return `${value / BASE_FONT_SIZE}rem`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If it is a string, return it as-is
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// For other types (objects, booleans, etc.)
|
|
61
|
+
return String(value);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Converts a value to pixels if it is a number, or keeps strings
|
|
66
|
+
* @param {number|string} value - Value to convert
|
|
67
|
+
* @returns {string} Value in pixels or original string
|
|
68
|
+
* @example
|
|
69
|
+
* toPX(16) // "16px"
|
|
70
|
+
* toPX('1rem') // "1rem"
|
|
71
|
+
* toPX('50%') // "50%"
|
|
72
|
+
*/
|
|
73
|
+
export const toPX = (value) => {
|
|
74
|
+
if (value === undefined || value === null) return undefined;
|
|
75
|
+
|
|
76
|
+
if (typeof value === "number") {
|
|
77
|
+
return `${value}px`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Gets the current base size
|
|
85
|
+
* @returns {number} Base size in pixels
|
|
86
|
+
*/
|
|
87
|
+
export const getBaseFontSize = () => BASE_FONT_SIZE;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// utils/visualEffects.js
|
|
2
|
+
/**
|
|
3
|
+
* Visual Effects - Reusable visual effects for widgets
|
|
4
|
+
* @module utils/visualEffects
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Cache for injected keyframes
|
|
8
|
+
const injectedKeyframes = new Set();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Inject CSS keyframes if not already injected
|
|
12
|
+
* @param {string} name - Animation name
|
|
13
|
+
* @param {string} keyframes - CSS keyframes
|
|
14
|
+
*/
|
|
15
|
+
export const injectKeyframes = (name, keyframes) => {
|
|
16
|
+
if (injectedKeyframes.has(name)) return;
|
|
17
|
+
const style = document.createElement("style");
|
|
18
|
+
style.textContent = `@keyframes ${name} { ${keyframes} }`;
|
|
19
|
+
document.head.appendChild(style);
|
|
20
|
+
injectedKeyframes.add(name);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply stripe pattern to an element
|
|
25
|
+
* @param {HTMLElement} element - Target element
|
|
26
|
+
* @param {Object} options - Stripe options
|
|
27
|
+
*/
|
|
28
|
+
export const applyStripes = (element, options = {}) => {
|
|
29
|
+
const {
|
|
30
|
+
color = "rgba(255, 255, 255, 0.15)",
|
|
31
|
+
size = 10,
|
|
32
|
+
angle = 45,
|
|
33
|
+
animated = false,
|
|
34
|
+
duration = "0.5s",
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const stripePattern = `repeating-linear-gradient(
|
|
38
|
+
${angle}deg,
|
|
39
|
+
${color} 0px,
|
|
40
|
+
${color} ${size}px,
|
|
41
|
+
transparent ${size}px,
|
|
42
|
+
transparent ${size * 2}px
|
|
43
|
+
)`;
|
|
44
|
+
|
|
45
|
+
element.style.backgroundImage = stripePattern;
|
|
46
|
+
element.style.backgroundSize = `${size * 2}px ${size * 2}px`;
|
|
47
|
+
|
|
48
|
+
if (animated) {
|
|
49
|
+
element.style.animation = `progress-stripes ${duration} linear infinite`;
|
|
50
|
+
|
|
51
|
+
injectKeyframes(
|
|
52
|
+
"progress-stripes",
|
|
53
|
+
`
|
|
54
|
+
0% { background-position: 0 0; }
|
|
55
|
+
100% { background-position: ${size * 2}px 0; }
|
|
56
|
+
`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Remove stripe pattern from element
|
|
63
|
+
*/
|
|
64
|
+
export const removeStripes = (element) => {
|
|
65
|
+
element.style.backgroundImage = "";
|
|
66
|
+
element.style.animation = "";
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply shimmer/skeleton effect
|
|
71
|
+
*/
|
|
72
|
+
export const applyShimmer = (element, options = {}) => {
|
|
73
|
+
const {
|
|
74
|
+
highlightColor = "rgba(255, 255, 255, 0.3)",
|
|
75
|
+
baseColor = "rgba(0, 0, 0, 0.05)",
|
|
76
|
+
duration = "1.5s",
|
|
77
|
+
} = options;
|
|
78
|
+
|
|
79
|
+
element.style.background = `linear-gradient(
|
|
80
|
+
110deg,
|
|
81
|
+
${baseColor} 0%,
|
|
82
|
+
${baseColor} 35%,
|
|
83
|
+
${highlightColor} 50%,
|
|
84
|
+
${baseColor} 65%,
|
|
85
|
+
${baseColor} 100%
|
|
86
|
+
)`;
|
|
87
|
+
element.style.backgroundSize = "200% 100%";
|
|
88
|
+
element.style.animation = `shimmer ${duration} ease-in-out infinite`;
|
|
89
|
+
|
|
90
|
+
injectKeyframes(
|
|
91
|
+
"shimmer",
|
|
92
|
+
`
|
|
93
|
+
0% { background-position: 200% 0; }
|
|
94
|
+
100% { background-position: -200% 0; }
|
|
95
|
+
`,
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply glow effect
|
|
101
|
+
*/
|
|
102
|
+
export const applyGlow = (element, options = {}) => {
|
|
103
|
+
const {
|
|
104
|
+
color = "rgba(0, 122, 255, 0.5)",
|
|
105
|
+
intensity = 10,
|
|
106
|
+
duration = "2s",
|
|
107
|
+
} = options;
|
|
108
|
+
|
|
109
|
+
element.style.transition = "box-shadow 0.2s ease";
|
|
110
|
+
element.style.animation = `glow-pulse ${duration} ease-in-out infinite`;
|
|
111
|
+
|
|
112
|
+
injectKeyframes(
|
|
113
|
+
"glow-pulse",
|
|
114
|
+
`
|
|
115
|
+
0% { box-shadow: 0 0 0 ${color}; }
|
|
116
|
+
50% { box-shadow: 0 0 ${intensity}px ${color}; }
|
|
117
|
+
100% { box-shadow: 0 0 0 ${color}; }
|
|
118
|
+
`,
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Apply indeterminate loading animation
|
|
124
|
+
*/
|
|
125
|
+
export const applyIndeterminate = (element, options = {}) => {
|
|
126
|
+
const { duration = "1.5s", width = "50%" } = options;
|
|
127
|
+
|
|
128
|
+
element.style.position = "relative";
|
|
129
|
+
element.style.width = width;
|
|
130
|
+
element.style.animation = `indeterminate-progress ${duration} ease-in-out infinite`;
|
|
131
|
+
|
|
132
|
+
injectKeyframes(
|
|
133
|
+
"indeterminate-progress",
|
|
134
|
+
`
|
|
135
|
+
0% { transform: translateX(-100%); }
|
|
136
|
+
50% { transform: translateX(0%); }
|
|
137
|
+
100% { transform: translateX(100%); }
|
|
138
|
+
`,
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply pulse animation
|
|
144
|
+
*/
|
|
145
|
+
export const applyPulse = (element, options = {}) => {
|
|
146
|
+
const { scale = 1.05, duration = "0.3s" } = options;
|
|
147
|
+
|
|
148
|
+
element.style.transition = `transform ${duration} ease`;
|
|
149
|
+
element.style.animation = `pulse ${duration} ease-in-out`;
|
|
150
|
+
|
|
151
|
+
injectKeyframes(
|
|
152
|
+
"pulse",
|
|
153
|
+
`
|
|
154
|
+
0% { transform: scale(1); }
|
|
155
|
+
50% { transform: scale(${scale}); }
|
|
156
|
+
100% { transform: scale(1); }
|
|
157
|
+
`,
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default {
|
|
162
|
+
injectKeyframes,
|
|
163
|
+
applyStripes,
|
|
164
|
+
removeStripes,
|
|
165
|
+
applyShimmer,
|
|
166
|
+
applyGlow,
|
|
167
|
+
applyIndeterminate,
|
|
168
|
+
applyPulse,
|
|
169
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// core/addChildren.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tracks which child nodes were appended via props (`child` / `children`).
|
|
5
|
+
* Using a WeakSet (external to the nodes) works with any Node type, including
|
|
6
|
+
* plain Text nodes, which cannot carry extra properties.
|
|
7
|
+
*
|
|
8
|
+
* @type {WeakSet<Node>}
|
|
9
|
+
*/
|
|
10
|
+
const PROP_CHILDREN = new WeakSet();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Appends one or more children to a parent widget.
|
|
14
|
+
*
|
|
15
|
+
* Accepts a single child or an array of children. Each item may be:
|
|
16
|
+
* - A DOM Node (appended directly).
|
|
17
|
+
* - A string or number (converted to a TextNode).
|
|
18
|
+
* - `null`, `undefined`, or `false` (skipped silently).
|
|
19
|
+
*
|
|
20
|
+
* When `markAsProp` is `true` (the default), every appended node is recorded
|
|
21
|
+
* in `PROP_CHILDREN` so that `removePropChildren` can selectively remove only
|
|
22
|
+
* those nodes later.
|
|
23
|
+
*
|
|
24
|
+
* @param {HTMLElement} parent - The parent widget to append children to.
|
|
25
|
+
* @param {Node|string|number|Array|null|undefined|false} children - Child or
|
|
26
|
+
* array of children to append.
|
|
27
|
+
* @param {Object} [options={}] - Options.
|
|
28
|
+
* @param {boolean} [options.markAsProp=true] - Whether to mark appended nodes
|
|
29
|
+
* as prop-managed so they can be cleaned up by `removePropChildren`.
|
|
30
|
+
* @returns {HTMLElement} The parent widget (for chaining).
|
|
31
|
+
*/
|
|
32
|
+
export const addChildren = (parent, children, options = {}) => {
|
|
33
|
+
if (!children && children !== 0) return parent;
|
|
34
|
+
|
|
35
|
+
const { markAsProp = true } = options;
|
|
36
|
+
const items = Array.isArray(children) ? children : [children];
|
|
37
|
+
|
|
38
|
+
items.forEach((child) => {
|
|
39
|
+
if (child === null || child === undefined || child === false) return;
|
|
40
|
+
|
|
41
|
+
let node = child;
|
|
42
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
43
|
+
node = document.createTextNode(String(child));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (markAsProp) {
|
|
47
|
+
PROP_CHILDREN.add(node);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
parent.appendChild(node);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return parent;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Removes all children that were previously appended via props (i.e. tracked in
|
|
58
|
+
* `PROP_CHILDREN`).
|
|
59
|
+
*
|
|
60
|
+
* Handles two cases:
|
|
61
|
+
* 1. **Normal path** – the node is still a direct child of `parent`. The
|
|
62
|
+
* overridden `removeChild` is used, which also updates `_children`.
|
|
63
|
+
* 2. **Fallback path** – the node was moved to a different parent (e.g. by
|
|
64
|
+
* drag-and-drop). `_children` is updated manually and the widget's `parent`
|
|
65
|
+
* reference is cleared.
|
|
66
|
+
*
|
|
67
|
+
* In both cases the node is removed from `PROP_CHILDREN` to allow future
|
|
68
|
+
* garbage collection.
|
|
69
|
+
*
|
|
70
|
+
* @param {HTMLElement} parent - The parent widget whose prop-children should be
|
|
71
|
+
* removed.
|
|
72
|
+
* @returns {HTMLElement} The parent widget (for chaining).
|
|
73
|
+
*/
|
|
74
|
+
export const removePropChildren = (parent) => {
|
|
75
|
+
const toRemove = Array.from(parent.childNodes).filter((child) =>
|
|
76
|
+
PROP_CHILDREN.has(child)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
toRemove.forEach((child) => {
|
|
80
|
+
if (child.parentNode === parent) {
|
|
81
|
+
// Normal path: removeChild override also cleans up _children.
|
|
82
|
+
parent.removeChild(child);
|
|
83
|
+
} else {
|
|
84
|
+
// Fallback: node was moved; manually clean up _children.
|
|
85
|
+
const idx = parent._children.indexOf(child);
|
|
86
|
+
if (idx !== -1) parent._children.splice(idx, 1);
|
|
87
|
+
if (child._isWidget) child.parent = null;
|
|
88
|
+
}
|
|
89
|
+
PROP_CHILDREN.delete(child);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return parent;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Removes ALL children from a parent widget, regardless of whether they were
|
|
97
|
+
* appended via props or programmatically.
|
|
98
|
+
*
|
|
99
|
+
* Resets `_children` to an empty array and clears every node from
|
|
100
|
+
* `PROP_CHILDREN` as it is removed.
|
|
101
|
+
*
|
|
102
|
+
* @param {HTMLElement} parent - The parent widget to clear.
|
|
103
|
+
* @returns {HTMLElement} The parent widget (for chaining).
|
|
104
|
+
*/
|
|
105
|
+
export const clearChildren = (parent) => {
|
|
106
|
+
while (parent.firstChild) {
|
|
107
|
+
PROP_CHILDREN.delete(parent.firstChild);
|
|
108
|
+
parent.removeChild(parent.firstChild);
|
|
109
|
+
}
|
|
110
|
+
parent._children = [];
|
|
111
|
+
return parent;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default { addChildren, removePropChildren, clearChildren };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// core/assignProps.js
|
|
2
|
+
import { setStyles } from "./tools.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the real value from a prop that may be a reactive accessor or a
|
|
6
|
+
* plain reactive object.
|
|
7
|
+
*
|
|
8
|
+
* - If the value is a function flagged with `_isReactive`, calls it to get the
|
|
9
|
+
* current value.
|
|
10
|
+
* - If the value is a reactive object (has `_isReactive === true`), returns
|
|
11
|
+
* `_value` when available, otherwise falls back to `valueOf()`.
|
|
12
|
+
* - Otherwise returns the value unchanged.
|
|
13
|
+
*
|
|
14
|
+
* @param {*} val - The raw prop value (may be reactive or plain).
|
|
15
|
+
* @returns {*} The resolved runtime value.
|
|
16
|
+
*/
|
|
17
|
+
const getValue = (val) => {
|
|
18
|
+
if (typeof val === "function" && val._isReactive) {
|
|
19
|
+
return val();
|
|
20
|
+
}
|
|
21
|
+
if (val && typeof val === "object" && val._isReactive === true) {
|
|
22
|
+
return val._value !== undefined ? val._value : val.valueOf();
|
|
23
|
+
}
|
|
24
|
+
return val;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HTML attributes whose mere presence means "true". Writing them through
|
|
29
|
+
* `setAttribute` with a falsy value is wrong — `setAttribute("readonly", false)`
|
|
30
|
+
* leaves the attribute present (`readonly="false"`) and the field stays
|
|
31
|
+
* read-only. These are written as DOM properties instead, so passing `false`
|
|
32
|
+
* removes the attribute.
|
|
33
|
+
*
|
|
34
|
+
* @type {Set<string>}
|
|
35
|
+
*/
|
|
36
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
37
|
+
"disabled",
|
|
38
|
+
"checked",
|
|
39
|
+
"selected",
|
|
40
|
+
"readOnly",
|
|
41
|
+
"required",
|
|
42
|
+
"autofocus",
|
|
43
|
+
"hidden",
|
|
44
|
+
"draggable",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Applies a single "special" prop directly to the widget. Handles cases that
|
|
49
|
+
* are not pure CSS assignments, such as `value` for form elements and `variant`
|
|
50
|
+
* as a dataset attribute.
|
|
51
|
+
*
|
|
52
|
+
* @param {HTMLElement} widget - The target DOM element.
|
|
53
|
+
* @param {string} key - The special prop name (e.g. `'variant'`).
|
|
54
|
+
* @param {*} value - The raw value (resolved via `getValue` internally).
|
|
55
|
+
* @returns {void}
|
|
56
|
+
*/
|
|
57
|
+
const applySpecial = (widget, key, value) => {
|
|
58
|
+
const val = getValue(value);
|
|
59
|
+
|
|
60
|
+
switch (key) {
|
|
61
|
+
case "value":
|
|
62
|
+
if (widget.tagName === "INPUT" || widget.tagName === "TEXTAREA") {
|
|
63
|
+
if (widget.value !== val) widget.value = val;
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "variant":
|
|
67
|
+
widget.dataset.variant = val;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Applies a processed props object to a widget element.
|
|
74
|
+
*
|
|
75
|
+
* Handles five prop categories in order:
|
|
76
|
+
* 1. **style** – merged into the element's inline style via `setStyles`.
|
|
77
|
+
* 2. **textContent** – set as `value` on form elements, `textContent` elsewhere.
|
|
78
|
+
* 3. **events** – previous listeners are removed and the new set is attached.
|
|
79
|
+
* `click` is assigned via `onclick` for reliable override behaviour.
|
|
80
|
+
* 4. **attributes** – applied with `setAttribute`, with special handling for
|
|
81
|
+
* `className` and boolean attributes (`disabled`, `readOnly`, `hidden`, …).
|
|
82
|
+
* 5. **special** – delegated to `applySpecial` (e.g. `value`, `variant`).
|
|
83
|
+
*
|
|
84
|
+
* Any keys not in the known set are handled as a flat-prop fallback: `on*`
|
|
85
|
+
* functions become event listeners, common HTML attributes are set directly, and
|
|
86
|
+
* everything else is tried as an inline style.
|
|
87
|
+
*
|
|
88
|
+
* The `_updating` guard prevents re-entrant calls from triggering reactive
|
|
89
|
+
* side-effects during the assignment.
|
|
90
|
+
*
|
|
91
|
+
* @param {HTMLElement} widget - The target DOM element.
|
|
92
|
+
* @param {Object|null} props - Processed props object from `processProps`, or
|
|
93
|
+
* `null` / `undefined` to no-op.
|
|
94
|
+
* @returns {HTMLElement} The same widget (for chaining).
|
|
95
|
+
*/
|
|
96
|
+
export const assignProps = (widget, props) => {
|
|
97
|
+
if (!props) return widget;
|
|
98
|
+
|
|
99
|
+
const wasUpdating = widget._updating;
|
|
100
|
+
widget._updating = true;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// ============================================================
|
|
104
|
+
// 1. STYLES
|
|
105
|
+
// ============================================================
|
|
106
|
+
if (props.style && Object.keys(props.style).length > 0) {
|
|
107
|
+
setStyles(widget, props.style);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ============================================================
|
|
111
|
+
// 2. TEXT
|
|
112
|
+
// ============================================================
|
|
113
|
+
if (props.textContent !== undefined) {
|
|
114
|
+
const textValue = getValue(props.textContent);
|
|
115
|
+
if (widget.tagName === "INPUT" || widget.tagName === "TEXTAREA") {
|
|
116
|
+
if (widget.value !== textValue) widget.value = textValue;
|
|
117
|
+
} else {
|
|
118
|
+
if (widget.textContent !== textValue) widget.textContent = textValue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// 3. EVENTS
|
|
124
|
+
// ============================================================
|
|
125
|
+
// Remove all previously registered listeners before adding the new ones.
|
|
126
|
+
if (widget._events && widget._events.length > 0) {
|
|
127
|
+
widget._events.forEach(({ event, handler }) => {
|
|
128
|
+
widget.removeEventListener(event, handler);
|
|
129
|
+
});
|
|
130
|
+
widget._events = [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
widget.onclick = null;
|
|
134
|
+
|
|
135
|
+
if (props.events && Object.keys(props.events).length > 0) {
|
|
136
|
+
Object.entries(props.events).forEach(([event, handler]) => {
|
|
137
|
+
if (typeof handler !== "function") return;
|
|
138
|
+
if (event === "click") {
|
|
139
|
+
widget.onclick = handler;
|
|
140
|
+
} else {
|
|
141
|
+
widget.addEventListener(event, handler);
|
|
142
|
+
widget._events.push({ event, handler });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================
|
|
148
|
+
// 4. ATTRIBUTES
|
|
149
|
+
// ============================================================
|
|
150
|
+
if (props.attributes && Object.keys(props.attributes).length > 0) {
|
|
151
|
+
Object.entries(props.attributes).forEach(([key, value]) => {
|
|
152
|
+
const finalValue = getValue(value);
|
|
153
|
+
try {
|
|
154
|
+
if (key === "className" || key === "class") {
|
|
155
|
+
widget.className = finalValue;
|
|
156
|
+
} else if (BOOLEAN_ATTRIBUTES.has(key)) {
|
|
157
|
+
widget[key] =
|
|
158
|
+
typeof finalValue === "boolean"
|
|
159
|
+
? finalValue
|
|
160
|
+
: Boolean(finalValue);
|
|
161
|
+
} else {
|
|
162
|
+
widget.setAttribute(key, finalValue);
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// Invalid attribute – silently skip
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================
|
|
171
|
+
// 5. SPECIALS
|
|
172
|
+
// ============================================================
|
|
173
|
+
if (props.special) {
|
|
174
|
+
for (const [key, value] of Object.entries(props.special)) {
|
|
175
|
+
applySpecial(widget, key, value);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ============================================================
|
|
180
|
+
// 6. FALLBACK FOR FLAT PROPS
|
|
181
|
+
// ============================================================
|
|
182
|
+
// Props not belonging to any known category are handled here as a best-
|
|
183
|
+
// effort: event handlers, common HTML attributes, and inline CSS.
|
|
184
|
+
const knownKeys = new Set([
|
|
185
|
+
"style",
|
|
186
|
+
"events",
|
|
187
|
+
"attributes",
|
|
188
|
+
"textContent",
|
|
189
|
+
"special",
|
|
190
|
+
"child",
|
|
191
|
+
"children",
|
|
192
|
+
"ref",
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
const extraKeys = Object.keys(props).filter((key) => !knownKeys.has(key));
|
|
196
|
+
if (extraKeys.length > 0) {
|
|
197
|
+
for (const key of extraKeys) {
|
|
198
|
+
const value = props[key];
|
|
199
|
+
if (value === undefined || value === null) continue;
|
|
200
|
+
|
|
201
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
202
|
+
const eventName = key.slice(2).toLowerCase();
|
|
203
|
+
if (eventName === "click") {
|
|
204
|
+
widget.onclick = value;
|
|
205
|
+
} else {
|
|
206
|
+
widget.addEventListener(eventName, value);
|
|
207
|
+
widget._events.push({ event: eventName, handler: value });
|
|
208
|
+
}
|
|
209
|
+
} else if (
|
|
210
|
+
[
|
|
211
|
+
"className",
|
|
212
|
+
"class",
|
|
213
|
+
"id",
|
|
214
|
+
"type",
|
|
215
|
+
"href",
|
|
216
|
+
"src",
|
|
217
|
+
"alt",
|
|
218
|
+
"title",
|
|
219
|
+
"role",
|
|
220
|
+
].includes(key)
|
|
221
|
+
) {
|
|
222
|
+
const finalValue = getValue(value);
|
|
223
|
+
if (key === "className" || key === "class") {
|
|
224
|
+
widget.className = finalValue;
|
|
225
|
+
} else {
|
|
226
|
+
try {
|
|
227
|
+
widget.setAttribute(key, finalValue);
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
}
|
|
230
|
+
} else if (key === "text" || key === "label" || key === "caption") {
|
|
231
|
+
const textValue = getValue(value);
|
|
232
|
+
if (widget.tagName === "INPUT" || widget.tagName === "TEXTAREA") {
|
|
233
|
+
if (widget.value !== textValue) widget.value = textValue;
|
|
234
|
+
} else {
|
|
235
|
+
if (widget.textContent !== textValue) widget.textContent = textValue;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const finalValue = getValue(value);
|
|
239
|
+
const cssValue =
|
|
240
|
+
typeof finalValue === "number" ? finalValue + "px" : finalValue;
|
|
241
|
+
try {
|
|
242
|
+
widget.style[key] = cssValue;
|
|
243
|
+
} catch (e) {}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error("❌ [assignProps] Error:", error);
|
|
249
|
+
} finally {
|
|
250
|
+
widget._updating = wasUpdating;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return widget;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export default assignProps;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// core/createWidget.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a bare DOM element for use as a widget.
|
|
5
|
+
*
|
|
6
|
+
* This is the lowest-level factory step. It validates the tag, creates the
|
|
7
|
+
* element, and initialises the `_events` array that all higher-level modules
|
|
8
|
+
* rely on for listener tracking. No props are applied here — that is the
|
|
9
|
+
* responsibility of `assignProps`.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} tag - HTML tag name for the element (e.g. `'div'`, `'button'`).
|
|
12
|
+
* Defaults to `'div'` when the value is not a string.
|
|
13
|
+
* @param {Object} props - Reserved for future use; currently unused.
|
|
14
|
+
* @returns {HTMLElement} The newly created DOM element with `_events` initialised.
|
|
15
|
+
*/
|
|
16
|
+
export const createWidget = (tag, props) => {
|
|
17
|
+
// Validateste that tag is a string; fall back to 'div' when it is not.
|
|
18
|
+
let finalTag = tag;
|
|
19
|
+
if (typeof finalTag !== "string") {
|
|
20
|
+
console.warn("⚠️ createWidget: tag is not a string, using 'div'", finalTag);
|
|
21
|
+
finalTag = "div";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const widget = document.createElement(finalTag);
|
|
25
|
+
widget._events = [];
|
|
26
|
+
|
|
27
|
+
return widget;
|
|
28
|
+
};
|