juxscript 1.0.3 → 1.0.4
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 +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +228 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1697 -388
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +166 -78
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +247 -0
- package/lib/components/sidebar.ts +86 -36
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +172 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request information and utilities
|
|
3
|
+
* Provides access to URL, query params, path, referrer, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface RequestInfo {
|
|
7
|
+
url: string;
|
|
8
|
+
path: string;
|
|
9
|
+
query: Record<string, string>;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
hash: string;
|
|
12
|
+
referrer: string;
|
|
13
|
+
method: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Req {
|
|
18
|
+
private static _instance: Req | null = null;
|
|
19
|
+
private _info: RequestInfo;
|
|
20
|
+
|
|
21
|
+
private constructor() {
|
|
22
|
+
this._info = this._parseRequest();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Singleton instance
|
|
27
|
+
*/
|
|
28
|
+
static get instance(): Req {
|
|
29
|
+
if (!Req._instance) {
|
|
30
|
+
Req._instance = new Req();
|
|
31
|
+
}
|
|
32
|
+
return Req._instance;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse current request information
|
|
37
|
+
*/
|
|
38
|
+
private _parseRequest(): RequestInfo {
|
|
39
|
+
const url = new URL(window.location.href);
|
|
40
|
+
|
|
41
|
+
// Parse query string
|
|
42
|
+
const query: Record<string, string> = {};
|
|
43
|
+
url.searchParams.forEach((value, key) => {
|
|
44
|
+
query[key] = value;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Parse path segments as params (basic routing)
|
|
48
|
+
const pathSegments = url.pathname.split('/').filter(s => s);
|
|
49
|
+
const params: Record<string, string> = {};
|
|
50
|
+
pathSegments.forEach((segment, index) => {
|
|
51
|
+
params[`${index}`] = segment;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
url: url.href,
|
|
56
|
+
path: url.pathname,
|
|
57
|
+
query,
|
|
58
|
+
params,
|
|
59
|
+
hash: url.hash.slice(1), // Remove leading #
|
|
60
|
+
referrer: document.referrer,
|
|
61
|
+
method: 'GET', // Browser requests are always GET initially
|
|
62
|
+
headers: {
|
|
63
|
+
'user-agent': navigator.userAgent,
|
|
64
|
+
'accept-language': navigator.language
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get full URL
|
|
71
|
+
*/
|
|
72
|
+
get url(): string {
|
|
73
|
+
return this._info.url;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get current path (e.g., "/examples/sample/dashboards")
|
|
78
|
+
*/
|
|
79
|
+
get path(): string {
|
|
80
|
+
return this._info.path;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get query string parameters
|
|
85
|
+
*/
|
|
86
|
+
get query(): Record<string, string> {
|
|
87
|
+
return { ...this._info.query };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get path parameters
|
|
92
|
+
*/
|
|
93
|
+
get params(): Record<string, string> {
|
|
94
|
+
return { ...this._info.params };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get URL hash (without #)
|
|
99
|
+
*/
|
|
100
|
+
get hash(): string {
|
|
101
|
+
return this._info.hash;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get referrer URL
|
|
106
|
+
*/
|
|
107
|
+
get referrer(): string {
|
|
108
|
+
return this._info.referrer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get request method (always GET for browser)
|
|
113
|
+
*/
|
|
114
|
+
get method(): string {
|
|
115
|
+
return this._info.method;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get request headers
|
|
120
|
+
*/
|
|
121
|
+
get headers(): Record<string, string> {
|
|
122
|
+
return { ...this._info.headers };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if current path matches a pattern
|
|
127
|
+
* @param pattern - Path pattern (supports wildcards)
|
|
128
|
+
*
|
|
129
|
+
* Examples:
|
|
130
|
+
* req.matches('/examples/sample/*')
|
|
131
|
+
* req.matches('/examples/sample/dashboards')
|
|
132
|
+
* req.matches('/examples/*\/dashboards')
|
|
133
|
+
*/
|
|
134
|
+
matches(pattern: string): boolean {
|
|
135
|
+
const regexPattern = pattern
|
|
136
|
+
.replace(/\*/g, '.*')
|
|
137
|
+
.replace(/\//g, '\\/');
|
|
138
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
139
|
+
return regex.test(this.path);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if current path starts with prefix
|
|
144
|
+
*/
|
|
145
|
+
startsWith(prefix: string): boolean {
|
|
146
|
+
return this.path.startsWith(prefix);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if current path ends with suffix
|
|
151
|
+
*/
|
|
152
|
+
endsWith(suffix: string): boolean {
|
|
153
|
+
return this.path.endsWith(suffix);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get query parameter value
|
|
158
|
+
*/
|
|
159
|
+
getQuery(key: string, defaultValue?: string): string | undefined {
|
|
160
|
+
return this.query[key] ?? defaultValue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get path parameter value
|
|
165
|
+
*/
|
|
166
|
+
getParam(key: string, defaultValue?: string): string | undefined {
|
|
167
|
+
return this.params[key] ?? defaultValue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if query parameter exists
|
|
172
|
+
*/
|
|
173
|
+
hasQuery(key: string): boolean {
|
|
174
|
+
return key in this.query;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get all query parameters as URLSearchParams
|
|
179
|
+
*/
|
|
180
|
+
getSearchParams(): URLSearchParams {
|
|
181
|
+
return new URLSearchParams(this.query);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Refresh request info (call after navigation)
|
|
186
|
+
*/
|
|
187
|
+
refresh(): void {
|
|
188
|
+
this._info = this._parseRequest();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set active state on nav items based on current path
|
|
193
|
+
* @param items - Nav items to update
|
|
194
|
+
* @returns Updated nav items with active state set
|
|
195
|
+
*/
|
|
196
|
+
setActiveNavItems(items: Array<{ href: string; active?: boolean }>): Array<{ href: string; active: boolean }> {
|
|
197
|
+
return items.map(item => ({
|
|
198
|
+
...item,
|
|
199
|
+
active: this.isActiveNavItem(item.href)
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a nav item should be active based on current path
|
|
205
|
+
* @param href - The nav item's href
|
|
206
|
+
*/
|
|
207
|
+
isActiveNavItem(href: string): boolean {
|
|
208
|
+
// Exact match
|
|
209
|
+
if (href === this.path) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Starts with match (for parent routes)
|
|
214
|
+
// e.g., "/examples/sample" matches "/examples/sample/dashboards"
|
|
215
|
+
if (href !== '/' && this.path.startsWith(href)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get breadcrumbs from current path
|
|
224
|
+
* @returns Array of breadcrumb objects with label and href
|
|
225
|
+
*/
|
|
226
|
+
getBreadcrumbs(): Array<{ label: string; href: string }> {
|
|
227
|
+
const segments = this.path.split('/').filter(s => s);
|
|
228
|
+
const breadcrumbs: Array<{ label: string; href: string }> = [
|
|
229
|
+
{ label: 'Home', href: '/' }
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
let currentPath = '';
|
|
233
|
+
segments.forEach(segment => {
|
|
234
|
+
currentPath += `/${segment}`;
|
|
235
|
+
breadcrumbs.push({
|
|
236
|
+
label: this._formatBreadcrumbLabel(segment),
|
|
237
|
+
href: currentPath
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return breadcrumbs;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Format segment into readable breadcrumb label
|
|
246
|
+
*/
|
|
247
|
+
private _formatBreadcrumbLabel(segment: string): string {
|
|
248
|
+
return segment
|
|
249
|
+
.split('-')
|
|
250
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
251
|
+
.join(' ');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if this is the home/root path
|
|
256
|
+
*/
|
|
257
|
+
get isHome(): boolean {
|
|
258
|
+
return this.path === '/' || this.path === '';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get current domain
|
|
263
|
+
*/
|
|
264
|
+
get domain(): string {
|
|
265
|
+
return window.location.hostname;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get current port
|
|
270
|
+
*/
|
|
271
|
+
get port(): string {
|
|
272
|
+
return window.location.port;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get current protocol
|
|
277
|
+
*/
|
|
278
|
+
get protocol(): string {
|
|
279
|
+
return window.location.protocol.replace(':', '');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if HTTPS
|
|
284
|
+
*/
|
|
285
|
+
get isSecure(): boolean {
|
|
286
|
+
return this.protocol === 'https';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Global request instance
|
|
292
|
+
*/
|
|
293
|
+
export const req = Req.instance;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Factory function (returns singleton)
|
|
297
|
+
*/
|
|
298
|
+
export function request(): Req {
|
|
299
|
+
return Req.instance;
|
|
300
|
+
}
|
package/lib/components/script.ts
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
1
|
import { ErrorHandler } from './error-handler.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Script - Inject JavaScript into the document
|
|
4
|
+
* Script - Inject inline JavaScript into the document
|
|
5
|
+
* For external scripts, use Import component instead
|
|
5
6
|
* Auto-renders when created or modified
|
|
6
7
|
*/
|
|
7
8
|
export class Script {
|
|
8
9
|
private _content: string = '';
|
|
9
|
-
private _src: string = '';
|
|
10
|
-
private _isSrc: boolean = false;
|
|
11
|
-
private _async: boolean = false;
|
|
12
|
-
private _defer: boolean = false;
|
|
13
10
|
private _type: string = '';
|
|
14
11
|
private _element: HTMLScriptElement | null = null;
|
|
15
12
|
|
|
16
|
-
constructor(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this._src = contentOrSrc;
|
|
22
|
-
this._isSrc = true;
|
|
23
|
-
} else {
|
|
24
|
-
this._content = contentOrSrc;
|
|
25
|
-
this._isSrc = false;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Auto-render if content/src provided
|
|
29
|
-
if (contentOrSrc) {
|
|
13
|
+
constructor(content: string = '') {
|
|
14
|
+
this._content = content;
|
|
15
|
+
|
|
16
|
+
// Auto-render if content provided
|
|
17
|
+
if (content) {
|
|
30
18
|
this.render();
|
|
31
19
|
}
|
|
32
20
|
}
|
|
@@ -36,52 +24,30 @@ export class Script {
|
|
|
36
24
|
*/
|
|
37
25
|
content(js: string): this {
|
|
38
26
|
this._content = js;
|
|
39
|
-
this._isSrc = false;
|
|
40
|
-
this._src = '';
|
|
41
27
|
this.render();
|
|
42
28
|
return this;
|
|
43
29
|
}
|
|
44
30
|
|
|
45
31
|
/**
|
|
46
|
-
* Set
|
|
47
|
-
*/
|
|
48
|
-
src(url: string): this {
|
|
49
|
-
this._src = url;
|
|
50
|
-
this._isSrc = true;
|
|
51
|
-
this._content = '';
|
|
52
|
-
this.render();
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Enable async loading (for external scripts)
|
|
58
|
-
*/
|
|
59
|
-
async(value: boolean = true): this {
|
|
60
|
-
this._async = value;
|
|
61
|
-
this.render();
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Enable defer loading (for external scripts)
|
|
32
|
+
* Set script type (e.g., 'module', 'text/javascript')
|
|
67
33
|
*/
|
|
68
|
-
|
|
69
|
-
this.
|
|
34
|
+
type(value: string): this {
|
|
35
|
+
this._type = value;
|
|
70
36
|
this.render();
|
|
71
37
|
return this;
|
|
72
38
|
}
|
|
73
39
|
|
|
74
40
|
/**
|
|
75
|
-
* Set
|
|
41
|
+
* Set as module script
|
|
76
42
|
*/
|
|
77
|
-
|
|
78
|
-
this._type =
|
|
43
|
+
module(): this {
|
|
44
|
+
this._type = 'module';
|
|
79
45
|
this.render();
|
|
80
46
|
return this;
|
|
81
47
|
}
|
|
82
48
|
|
|
83
49
|
/**
|
|
84
|
-
* Render the script element
|
|
50
|
+
* Render the inline script element
|
|
85
51
|
*/
|
|
86
52
|
render(): this {
|
|
87
53
|
if (typeof document === 'undefined') {
|
|
@@ -93,34 +59,16 @@ export class Script {
|
|
|
93
59
|
this.remove();
|
|
94
60
|
|
|
95
61
|
const script = document.createElement('script');
|
|
62
|
+
script.textContent = this._content;
|
|
96
63
|
|
|
97
|
-
if (this.
|
|
98
|
-
script.
|
|
99
|
-
|
|
100
|
-
// Add error handler for failed loads
|
|
101
|
-
script.onerror = () => {
|
|
102
|
-
ErrorHandler.captureError({
|
|
103
|
-
component: 'Script',
|
|
104
|
-
method: 'render',
|
|
105
|
-
message: `Failed to load script: ${this._src}`,
|
|
106
|
-
timestamp: new Date(),
|
|
107
|
-
context: { src: this._src, type: 'external', error: 'load_failed' }
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
script.onload = () => {
|
|
112
|
-
console.log(`✓ Script loaded: ${this._src}`);
|
|
113
|
-
};
|
|
114
|
-
} else {
|
|
115
|
-
script.textContent = this._content;
|
|
64
|
+
if (this._type) {
|
|
65
|
+
script.type = this._type;
|
|
116
66
|
}
|
|
117
67
|
|
|
118
|
-
if (this._async) script.async = true;
|
|
119
|
-
if (this._defer) script.defer = true;
|
|
120
|
-
if (this._type) script.type = this._type;
|
|
121
|
-
|
|
122
68
|
document.head.appendChild(script);
|
|
123
69
|
this._element = script;
|
|
70
|
+
|
|
71
|
+
console.log('✓ Inline script rendered');
|
|
124
72
|
} catch (error: any) {
|
|
125
73
|
ErrorHandler.captureError({
|
|
126
74
|
component: 'Script',
|
|
@@ -128,9 +76,8 @@ export class Script {
|
|
|
128
76
|
message: error.message,
|
|
129
77
|
stack: error.stack,
|
|
130
78
|
timestamp: new Date(),
|
|
131
|
-
context: {
|
|
132
|
-
|
|
133
|
-
src: this._src,
|
|
79
|
+
context: {
|
|
80
|
+
type: this._type || 'inline',
|
|
134
81
|
error: 'runtime_exception'
|
|
135
82
|
}
|
|
136
83
|
});
|
|
@@ -149,4 +96,16 @@ export class Script {
|
|
|
149
96
|
}
|
|
150
97
|
return this;
|
|
151
98
|
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Factory function
|
|
103
|
+
*
|
|
104
|
+
* Usage:
|
|
105
|
+
* jux.script('console.log("Hello")');
|
|
106
|
+
* jux.script().content('alert("Hi")');
|
|
107
|
+
* jux.script('export const x = 1;').module();
|
|
108
|
+
*/
|
|
109
|
+
export function script(content: string = ''): Script {
|
|
110
|
+
return new Script(content);
|
|
152
111
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Select option
|
|
6
|
+
*/
|
|
7
|
+
export interface SelectOption {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Select component options
|
|
15
|
+
*/
|
|
16
|
+
export interface SelectOptions {
|
|
17
|
+
options?: SelectOption[];
|
|
18
|
+
value?: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
name?: string;
|
|
22
|
+
onChange?: (value: string) => void;
|
|
23
|
+
style?: string;
|
|
24
|
+
class?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Select component state
|
|
29
|
+
*/
|
|
30
|
+
type SelectState = {
|
|
31
|
+
options: SelectOption[];
|
|
32
|
+
value: string;
|
|
33
|
+
placeholder: string;
|
|
34
|
+
disabled: boolean;
|
|
35
|
+
name: string;
|
|
36
|
+
style: string;
|
|
37
|
+
class: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Select component - Dropdown select
|
|
42
|
+
*
|
|
43
|
+
* Usage:
|
|
44
|
+
* jux.select('country', {
|
|
45
|
+
* placeholder: 'Select country',
|
|
46
|
+
* options: [
|
|
47
|
+
* { label: 'USA', value: 'us' },
|
|
48
|
+
* { label: 'Canada', value: 'ca' }
|
|
49
|
+
* ],
|
|
50
|
+
* onChange: (val) => console.log(val)
|
|
51
|
+
* }).render('#form');
|
|
52
|
+
*
|
|
53
|
+
* // Two-way binding
|
|
54
|
+
* const countryState = state('us');
|
|
55
|
+
* jux.select('country').bind(countryState).render('#form');
|
|
56
|
+
*/
|
|
57
|
+
export class Select {
|
|
58
|
+
state: SelectState;
|
|
59
|
+
container: HTMLElement | null = null;
|
|
60
|
+
_id: string;
|
|
61
|
+
id: string;
|
|
62
|
+
private _onChange?: (value: string) => void;
|
|
63
|
+
private _boundState?: State<string>;
|
|
64
|
+
|
|
65
|
+
constructor(id: string, options: SelectOptions = {}) {
|
|
66
|
+
this._id = id;
|
|
67
|
+
this.id = id;
|
|
68
|
+
this._onChange = options.onChange;
|
|
69
|
+
|
|
70
|
+
this.state = {
|
|
71
|
+
options: options.options ?? [],
|
|
72
|
+
value: options.value ?? '',
|
|
73
|
+
placeholder: options.placeholder ?? 'Select...',
|
|
74
|
+
disabled: options.disabled ?? false,
|
|
75
|
+
name: options.name ?? id,
|
|
76
|
+
style: options.style ?? '',
|
|
77
|
+
class: options.class ?? ''
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* -------------------------
|
|
82
|
+
* Fluent API
|
|
83
|
+
* ------------------------- */
|
|
84
|
+
|
|
85
|
+
options(value: SelectOption[]): this {
|
|
86
|
+
this.state.options = value;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addOption(option: SelectOption): this {
|
|
91
|
+
this.state.options = [...this.state.options, option];
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
value(value: string): this {
|
|
96
|
+
this.state.value = value;
|
|
97
|
+
this._updateElement();
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
placeholder(value: string): this {
|
|
102
|
+
this.state.placeholder = value;
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
disabled(value: boolean): this {
|
|
107
|
+
this.state.disabled = value;
|
|
108
|
+
this._updateElement();
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
name(value: string): this {
|
|
113
|
+
this.state.name = value;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
style(value: string): this {
|
|
118
|
+
this.state.style = value;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class(value: string): this {
|
|
123
|
+
this.state.class = value;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onChange(handler: (value: string) => void): this {
|
|
128
|
+
this._onChange = handler;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Two-way binding to state
|
|
134
|
+
*/
|
|
135
|
+
bind(stateObj: State<string>): this {
|
|
136
|
+
this._boundState = stateObj;
|
|
137
|
+
|
|
138
|
+
// Update select when state changes
|
|
139
|
+
stateObj.subscribe((val) => {
|
|
140
|
+
this.state.value = val;
|
|
141
|
+
this._updateElement();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Update state when select changes
|
|
145
|
+
this.onChange((value) => stateObj.set(value));
|
|
146
|
+
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* -------------------------
|
|
151
|
+
* Helpers
|
|
152
|
+
* ------------------------- */
|
|
153
|
+
|
|
154
|
+
private _updateElement(): void {
|
|
155
|
+
const select = document.getElementById(`${this._id}-select`) as HTMLSelectElement;
|
|
156
|
+
if (select) {
|
|
157
|
+
select.value = this.state.value;
|
|
158
|
+
select.disabled = this.state.disabled;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getValue(): string {
|
|
163
|
+
return this.state.value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* -------------------------
|
|
167
|
+
* Render
|
|
168
|
+
* ------------------------- */
|
|
169
|
+
|
|
170
|
+
render(targetId?: string): this {
|
|
171
|
+
let container: HTMLElement;
|
|
172
|
+
|
|
173
|
+
if (targetId) {
|
|
174
|
+
const target = document.querySelector(targetId);
|
|
175
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
176
|
+
throw new Error(`Select: Target element "${targetId}" not found`);
|
|
177
|
+
}
|
|
178
|
+
container = target;
|
|
179
|
+
} else {
|
|
180
|
+
container = getOrCreateContainer(this._id);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.container = container;
|
|
184
|
+
const { options, value, placeholder, disabled, name, style, class: className } = this.state;
|
|
185
|
+
|
|
186
|
+
const wrapper = document.createElement('div');
|
|
187
|
+
wrapper.className = 'jux-select';
|
|
188
|
+
wrapper.id = this._id;
|
|
189
|
+
|
|
190
|
+
if (className) {
|
|
191
|
+
wrapper.className += ` ${className}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (style) {
|
|
195
|
+
wrapper.setAttribute('style', style);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const select = document.createElement('select');
|
|
199
|
+
select.className = 'jux-select-element';
|
|
200
|
+
select.id = `${this._id}-select`;
|
|
201
|
+
select.name = name;
|
|
202
|
+
select.disabled = disabled;
|
|
203
|
+
|
|
204
|
+
// Placeholder option
|
|
205
|
+
if (placeholder) {
|
|
206
|
+
const placeholderOpt = document.createElement('option');
|
|
207
|
+
placeholderOpt.value = '';
|
|
208
|
+
placeholderOpt.textContent = placeholder;
|
|
209
|
+
placeholderOpt.disabled = true;
|
|
210
|
+
placeholderOpt.selected = value === '';
|
|
211
|
+
select.appendChild(placeholderOpt);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Options
|
|
215
|
+
options.forEach(opt => {
|
|
216
|
+
const option = document.createElement('option');
|
|
217
|
+
option.value = opt.value;
|
|
218
|
+
option.textContent = opt.label;
|
|
219
|
+
option.disabled = opt.disabled ?? false;
|
|
220
|
+
option.selected = opt.value === value;
|
|
221
|
+
select.appendChild(option);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
select.addEventListener('change', (e) => {
|
|
225
|
+
const target = e.target as HTMLSelectElement;
|
|
226
|
+
this.state.value = target.value;
|
|
227
|
+
if (this._onChange) {
|
|
228
|
+
this._onChange(target.value);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
wrapper.appendChild(select);
|
|
233
|
+
container.appendChild(wrapper);
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
renderTo(juxComponent: any): this {
|
|
238
|
+
if (!juxComponent?._id) {
|
|
239
|
+
throw new Error('Select.renderTo: Invalid component');
|
|
240
|
+
}
|
|
241
|
+
return this.render(`#${juxComponent._id}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function select(id: string, options: SelectOptions = {}): Select {
|
|
246
|
+
return new Select(id, options);
|
|
247
|
+
}
|