juxscript 1.0.18 → 1.0.20
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/lib/components/alert.ts +124 -128
- package/lib/components/areachart.ts +169 -287
- package/lib/components/areachartsmooth.ts +2 -2
- package/lib/components/badge.ts +63 -72
- package/lib/components/barchart.ts +120 -48
- package/lib/components/button.ts +99 -101
- package/lib/components/card.ts +97 -121
- package/lib/components/chart-types.ts +159 -0
- package/lib/components/chart-utils.ts +160 -0
- package/lib/components/chart.ts +628 -48
- package/lib/components/checkbox.ts +137 -51
- package/lib/components/code.ts +89 -75
- package/lib/components/container.ts +1 -1
- package/lib/components/datepicker.ts +93 -78
- package/lib/components/dialog.ts +163 -130
- package/lib/components/divider.ts +111 -193
- package/lib/components/docs-data.json +711 -264
- package/lib/components/doughnutchart.ts +125 -57
- package/lib/components/dropdown.ts +172 -85
- package/lib/components/element.ts +66 -61
- package/lib/components/fileupload.ts +142 -171
- package/lib/components/heading.ts +64 -21
- package/lib/components/hero.ts +109 -34
- package/lib/components/icon.ts +247 -0
- package/lib/components/icons.ts +174 -0
- package/lib/components/include.ts +77 -2
- package/lib/components/input.ts +174 -125
- package/lib/components/list.ts +120 -79
- package/lib/components/menu.ts +97 -2
- package/lib/components/modal.ts +144 -63
- package/lib/components/nav.ts +153 -52
- package/lib/components/paragraph.ts +78 -28
- package/lib/components/progress.ts +83 -107
- package/lib/components/radio.ts +151 -52
- package/lib/components/select.ts +110 -102
- package/lib/components/sidebar.ts +148 -105
- package/lib/components/switch.ts +124 -125
- package/lib/components/table.ts +214 -137
- package/lib/components/tabs.ts +194 -113
- package/lib/components/theme-toggle.ts +38 -7
- package/lib/components/tooltip.ts +207 -47
- package/lib/jux.ts +24 -5
- package/lib/reactivity/state.ts +13 -299
- package/package.json +1 -2
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Element component options
|
|
5
|
-
*/
|
|
6
4
|
export interface ElementOptions {
|
|
7
5
|
tagType?: string;
|
|
8
6
|
className?: string;
|
|
@@ -14,9 +12,6 @@ export interface ElementOptions {
|
|
|
14
12
|
class?: string;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
/**
|
|
18
|
-
* Element component state
|
|
19
|
-
*/
|
|
20
15
|
type ElementState = {
|
|
21
16
|
tagType: string;
|
|
22
17
|
className: string;
|
|
@@ -28,30 +23,15 @@ type ElementState = {
|
|
|
28
23
|
class: string;
|
|
29
24
|
};
|
|
30
25
|
|
|
31
|
-
/**
|
|
32
|
-
* Element component - Create arbitrary HTML elements
|
|
33
|
-
*
|
|
34
|
-
* Usage:
|
|
35
|
-
* const div = jux.element('myDiv', { tagType: 'div', className: 'container' });
|
|
36
|
-
* div.render('#target');
|
|
37
|
-
*
|
|
38
|
-
* const span = jux.element('mySpan')
|
|
39
|
-
* .tagType('span')
|
|
40
|
-
* .className('highlight')
|
|
41
|
-
* .text('Hello World')
|
|
42
|
-
* .render();
|
|
43
|
-
*
|
|
44
|
-
* For reactive content, use State bindings after render:
|
|
45
|
-
* const count = state(0);
|
|
46
|
-
* jux.element('counter').render('#app');
|
|
47
|
-
* count.bindText('counter', (val) => `Count: ${val}`);
|
|
48
|
-
*/
|
|
49
26
|
export class Element {
|
|
50
27
|
state: ElementState;
|
|
51
28
|
container: HTMLElement | null = null;
|
|
52
29
|
_id: string;
|
|
53
30
|
id: string;
|
|
54
31
|
|
|
32
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
33
|
+
private _syncBindings: Array<{ property: string, stateObj: State<any>, transform?: Function }> = [];
|
|
34
|
+
|
|
55
35
|
constructor(id: string, options: ElementOptions = {}) {
|
|
56
36
|
this._id = id;
|
|
57
37
|
this.id = id;
|
|
@@ -178,6 +158,25 @@ export class Element {
|
|
|
178
158
|
return this;
|
|
179
159
|
}
|
|
180
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Bind an event to a handler
|
|
163
|
+
*/
|
|
164
|
+
bind(event: string, handler: Function): Element {
|
|
165
|
+
this._bindings.push({ event, handler });
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Sync a property to a state object
|
|
171
|
+
*/
|
|
172
|
+
sync(property: string, stateObj: State<any>, transform?: Function): Element {
|
|
173
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
174
|
+
throw new Error(`Element.sync: Expected a State object for property "${property}"`);
|
|
175
|
+
}
|
|
176
|
+
this._syncBindings.push({ property, stateObj, transform });
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
|
|
181
180
|
/* -------------------------
|
|
182
181
|
* Render
|
|
183
182
|
* ------------------------- */
|
|
@@ -187,8 +186,8 @@ export class Element {
|
|
|
187
186
|
* @param targetId - CSS selector for target element (optional, defaults to body)
|
|
188
187
|
*/
|
|
189
188
|
render(targetId?: string): Element {
|
|
189
|
+
// === 1. SETUP: Get container ===
|
|
190
190
|
let container: HTMLElement;
|
|
191
|
-
|
|
192
191
|
if (targetId) {
|
|
193
192
|
const target = document.querySelector(targetId);
|
|
194
193
|
if (!target || !(target instanceof HTMLElement)) {
|
|
@@ -198,48 +197,62 @@ export class Element {
|
|
|
198
197
|
} else {
|
|
199
198
|
container = getOrCreateContainer(this._id);
|
|
200
199
|
}
|
|
201
|
-
|
|
202
200
|
this.container = container;
|
|
201
|
+
|
|
202
|
+
// === 2. PREPARE: Destructure state ===
|
|
203
203
|
const { tagType, className, text, innerHTML, attributes, styles, style, class: classValue } = this.state;
|
|
204
204
|
|
|
205
|
-
// Create
|
|
205
|
+
// === 3. BUILD: Create DOM element ===
|
|
206
206
|
const element = document.createElement(tagType);
|
|
207
|
-
|
|
208
|
-
// Set ID to component ID
|
|
209
207
|
element.id = this._id;
|
|
210
208
|
|
|
211
|
-
|
|
212
|
-
if (
|
|
213
|
-
element.className = className;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (classValue) {
|
|
217
|
-
element.className = element.className ? `${element.className} ${classValue}` : classValue;
|
|
218
|
-
}
|
|
209
|
+
if (className) element.className = className;
|
|
210
|
+
if (classValue) element.className = `${element.className} ${classValue}`.trim();
|
|
219
211
|
|
|
220
|
-
|
|
221
|
-
if (innerHTML) {
|
|
222
|
-
element.innerHTML = innerHTML;
|
|
223
|
-
} else if (text) {
|
|
212
|
+
if (text) {
|
|
224
213
|
element.textContent = text;
|
|
214
|
+
} else if (innerHTML) {
|
|
215
|
+
element.innerHTML = innerHTML;
|
|
225
216
|
}
|
|
226
217
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
element.setAttribute(name, value);
|
|
218
|
+
Object.entries(attributes).forEach(([key, value]) => {
|
|
219
|
+
element.setAttribute(key, value);
|
|
230
220
|
});
|
|
231
221
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
222
|
+
const styleString = Object.entries(styles)
|
|
223
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
224
|
+
.join('; ');
|
|
236
225
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
element.setAttribute('style', style);
|
|
226
|
+
if (styleString || style) {
|
|
227
|
+
element.setAttribute('style', `${styleString}; ${style}`.trim());
|
|
240
228
|
}
|
|
241
229
|
|
|
230
|
+
// === 4. WIRE: Add event listeners ===
|
|
231
|
+
|
|
232
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
233
|
+
element.addEventListener(event, handler as EventListener);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
this._syncBindings.forEach(({ property, stateObj, transform }) => {
|
|
237
|
+
stateObj.subscribe((val: any) => {
|
|
238
|
+
const transformed = transform ? transform(val) : val;
|
|
239
|
+
|
|
240
|
+
if (property === 'text') {
|
|
241
|
+
element.textContent = String(transformed);
|
|
242
|
+
this.state.text = String(transformed);
|
|
243
|
+
} else if (property === 'innerHTML') {
|
|
244
|
+
element.innerHTML = String(transformed);
|
|
245
|
+
this.state.innerHTML = String(transformed);
|
|
246
|
+
} else if (property === 'class') {
|
|
247
|
+
element.className = String(transformed);
|
|
248
|
+
this.state.class = String(transformed);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// === 5. RENDER: Append to DOM ===
|
|
242
254
|
container.appendChild(element);
|
|
255
|
+
|
|
243
256
|
return this;
|
|
244
257
|
}
|
|
245
258
|
|
|
@@ -251,21 +264,13 @@ export class Element {
|
|
|
251
264
|
* const child = jux.element('myChild').renderTo(container);
|
|
252
265
|
*/
|
|
253
266
|
renderTo(juxComponent: Element): Element {
|
|
254
|
-
if (!juxComponent ||
|
|
255
|
-
throw new Error('Element.renderTo: Invalid component
|
|
267
|
+
if (!juxComponent || !juxComponent._id) {
|
|
268
|
+
throw new Error('Element.renderTo: Invalid component');
|
|
256
269
|
}
|
|
257
|
-
|
|
258
|
-
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
259
|
-
throw new Error('Element.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
270
|
return this.render(`#${juxComponent._id}`);
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
273
|
|
|
266
|
-
/**
|
|
267
|
-
* Factory helper
|
|
268
|
-
*/
|
|
269
274
|
export function element(id: string, options: ElementOptions = {}): Element {
|
|
270
275
|
return new Element(id, options);
|
|
271
276
|
}
|
|
@@ -1,73 +1,55 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* FileUpload component options
|
|
5
|
-
*/
|
|
6
4
|
export interface FileUploadOptions {
|
|
5
|
+
label?: string;
|
|
7
6
|
accept?: string;
|
|
8
7
|
multiple?: boolean;
|
|
9
|
-
maxSize?: number;
|
|
10
8
|
disabled?: boolean;
|
|
11
|
-
|
|
12
|
-
dragText?: string;
|
|
13
|
-
onChange?: (files: FileList) => void;
|
|
14
|
-
onError?: (error: string) => void;
|
|
9
|
+
name?: string;
|
|
15
10
|
style?: string;
|
|
16
11
|
class?: string;
|
|
17
12
|
}
|
|
18
13
|
|
|
19
|
-
/**
|
|
20
|
-
* FileUpload component state
|
|
21
|
-
*/
|
|
22
14
|
type FileUploadState = {
|
|
15
|
+
files: File[];
|
|
16
|
+
label: string;
|
|
23
17
|
accept: string;
|
|
24
18
|
multiple: boolean;
|
|
25
|
-
maxSize: number;
|
|
26
19
|
disabled: boolean;
|
|
27
|
-
|
|
28
|
-
dragText: string;
|
|
20
|
+
name: string;
|
|
29
21
|
style: string;
|
|
30
22
|
class: string;
|
|
31
|
-
files: File[];
|
|
32
23
|
};
|
|
33
24
|
|
|
34
|
-
/**
|
|
35
|
-
* FileUpload component - Drag & drop file input
|
|
36
|
-
*
|
|
37
|
-
* Usage:
|
|
38
|
-
* jux.fileupload('docs', {
|
|
39
|
-
* accept: '.pdf,.doc,.docx',
|
|
40
|
-
* multiple: true,
|
|
41
|
-
* maxSize: 5242880, // 5MB
|
|
42
|
-
* text: 'Click to upload or drag and drop',
|
|
43
|
-
* onChange: (files) => console.log(files),
|
|
44
|
-
* onError: (err) => console.error(err)
|
|
45
|
-
* }).render('#form');
|
|
46
|
-
*/
|
|
47
25
|
export class FileUpload {
|
|
48
26
|
state: FileUploadState;
|
|
49
27
|
container: HTMLElement | null = null;
|
|
50
28
|
_id: string;
|
|
51
29
|
id: string;
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
|
|
31
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
32
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
33
|
+
private _syncBindings: Array<{
|
|
34
|
+
property: string,
|
|
35
|
+
stateObj: State<any>,
|
|
36
|
+
toState?: Function,
|
|
37
|
+
toComponent?: Function
|
|
38
|
+
}> = [];
|
|
54
39
|
|
|
55
40
|
constructor(id: string, options: FileUploadOptions = {}) {
|
|
56
41
|
this._id = id;
|
|
57
42
|
this.id = id;
|
|
58
|
-
this._onChange = options.onChange;
|
|
59
|
-
this._onError = options.onError;
|
|
60
43
|
|
|
61
44
|
this.state = {
|
|
62
|
-
|
|
45
|
+
files: [],
|
|
46
|
+
label: options.label ?? '',
|
|
47
|
+
accept: options.accept ?? '',
|
|
63
48
|
multiple: options.multiple ?? false,
|
|
64
|
-
maxSize: options.maxSize ?? 10485760, // 10MB default
|
|
65
49
|
disabled: options.disabled ?? false,
|
|
66
|
-
|
|
67
|
-
dragText: options.dragText ?? 'Drop files here',
|
|
50
|
+
name: options.name ?? id,
|
|
68
51
|
style: options.style ?? '',
|
|
69
|
-
class: options.class ?? ''
|
|
70
|
-
files: []
|
|
52
|
+
class: options.class ?? ''
|
|
71
53
|
};
|
|
72
54
|
}
|
|
73
55
|
|
|
@@ -75,6 +57,11 @@ export class FileUpload {
|
|
|
75
57
|
* Fluent API
|
|
76
58
|
* ------------------------- */
|
|
77
59
|
|
|
60
|
+
label(value: string): this {
|
|
61
|
+
this.state.label = value;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
78
65
|
accept(value: string): this {
|
|
79
66
|
this.state.accept = value;
|
|
80
67
|
return this;
|
|
@@ -85,23 +72,14 @@ export class FileUpload {
|
|
|
85
72
|
return this;
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
maxSize(value: number): this {
|
|
89
|
-
this.state.maxSize = value;
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
75
|
disabled(value: boolean): this {
|
|
94
76
|
this.state.disabled = value;
|
|
77
|
+
this._updateElement();
|
|
95
78
|
return this;
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
|
|
99
|
-
this.state.
|
|
100
|
-
return this;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
dragText(value: string): this {
|
|
104
|
-
this.state.dragText = value;
|
|
81
|
+
name(value: string): this {
|
|
82
|
+
this.state.name = value;
|
|
105
83
|
return this;
|
|
106
84
|
}
|
|
107
85
|
|
|
@@ -115,13 +93,16 @@ export class FileUpload {
|
|
|
115
93
|
return this;
|
|
116
94
|
}
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
this.
|
|
96
|
+
bind(event: string, handler: Function): this {
|
|
97
|
+
this._bindings.push({ event, handler });
|
|
120
98
|
return this;
|
|
121
99
|
}
|
|
122
100
|
|
|
123
|
-
|
|
124
|
-
|
|
101
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
102
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
103
|
+
throw new Error(`FileUpload.sync: Expected a State object for property "${property}"`);
|
|
104
|
+
}
|
|
105
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
125
106
|
return this;
|
|
126
107
|
}
|
|
127
108
|
|
|
@@ -129,61 +110,15 @@ export class FileUpload {
|
|
|
129
110
|
* Helpers
|
|
130
111
|
* ------------------------- */
|
|
131
112
|
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (file.size > this.state.maxSize) {
|
|
136
|
-
const maxMB = Math.round(this.state.maxSize / 1048576);
|
|
137
|
-
const error = `File "${file.name}" exceeds maximum size of ${maxMB}MB`;
|
|
138
|
-
if (this._onError) {
|
|
139
|
-
this._onError(error);
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}
|
|
113
|
+
private _updateElement(): void {
|
|
114
|
+
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
|
|
115
|
+
const button = document.getElementById(`${this._id}-button`) as HTMLButtonElement;
|
|
146
116
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.state.files = Array.from(files);
|
|
150
|
-
this._updateFileList();
|
|
151
|
-
if (this._onChange) {
|
|
152
|
-
this._onChange(files);
|
|
153
|
-
}
|
|
117
|
+
if (input) {
|
|
118
|
+
input.disabled = this.state.disabled;
|
|
154
119
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
private _updateFileList(): void {
|
|
158
|
-
const fileList = document.getElementById(`${this._id}-list`);
|
|
159
|
-
if (fileList) {
|
|
160
|
-
fileList.innerHTML = '';
|
|
161
|
-
this.state.files.forEach((file, index) => {
|
|
162
|
-
const item = document.createElement('div');
|
|
163
|
-
item.className = 'jux-fileupload-file';
|
|
164
|
-
|
|
165
|
-
const name = document.createElement('span');
|
|
166
|
-
name.className = 'jux-fileupload-file-name';
|
|
167
|
-
name.textContent = file.name;
|
|
168
|
-
|
|
169
|
-
const size = document.createElement('span');
|
|
170
|
-
size.className = 'jux-fileupload-file-size';
|
|
171
|
-
const sizeKB = Math.round(file.size / 1024);
|
|
172
|
-
size.textContent = `${sizeKB} KB`;
|
|
173
|
-
|
|
174
|
-
const removeBtn = document.createElement('button');
|
|
175
|
-
removeBtn.className = 'jux-fileupload-file-remove';
|
|
176
|
-
removeBtn.textContent = '×';
|
|
177
|
-
removeBtn.addEventListener('click', () => {
|
|
178
|
-
this.state.files.splice(index, 1);
|
|
179
|
-
this._updateFileList();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
item.appendChild(name);
|
|
183
|
-
item.appendChild(size);
|
|
184
|
-
item.appendChild(removeBtn);
|
|
185
|
-
fileList.appendChild(item);
|
|
186
|
-
});
|
|
120
|
+
if (button) {
|
|
121
|
+
button.disabled = this.state.disabled;
|
|
187
122
|
}
|
|
188
123
|
}
|
|
189
124
|
|
|
@@ -193,119 +128,155 @@ export class FileUpload {
|
|
|
193
128
|
|
|
194
129
|
clear(): void {
|
|
195
130
|
this.state.files = [];
|
|
196
|
-
this._updateFileList();
|
|
197
131
|
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
|
|
132
|
+
const fileList = document.getElementById(`${this._id}-list`);
|
|
133
|
+
|
|
198
134
|
if (input) {
|
|
199
135
|
input.value = '';
|
|
200
136
|
}
|
|
137
|
+
if (fileList) {
|
|
138
|
+
this._updateFileList(fileList, []);
|
|
139
|
+
}
|
|
201
140
|
}
|
|
202
141
|
|
|
203
142
|
/* -------------------------
|
|
204
|
-
* Render
|
|
143
|
+
* Render (5-Step Pattern)
|
|
205
144
|
* ------------------------- */
|
|
206
145
|
|
|
207
146
|
render(targetId?: string): this {
|
|
147
|
+
// === 1. SETUP: Get or create container ===
|
|
208
148
|
let container: HTMLElement;
|
|
209
|
-
|
|
210
149
|
if (targetId) {
|
|
211
150
|
const target = document.querySelector(targetId);
|
|
212
151
|
if (!target || !(target instanceof HTMLElement)) {
|
|
213
|
-
throw new Error(`FileUpload: Target
|
|
152
|
+
throw new Error(`FileUpload: Target "${targetId}" not found`);
|
|
214
153
|
}
|
|
215
154
|
container = target;
|
|
216
155
|
} else {
|
|
217
156
|
container = getOrCreateContainer(this._id);
|
|
218
157
|
}
|
|
219
|
-
|
|
220
158
|
this.container = container;
|
|
221
|
-
const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
|
|
222
159
|
|
|
160
|
+
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
161
|
+
const { label, accept, multiple, disabled, name, style, class: className } = this.state;
|
|
162
|
+
const hasFilesSync = this._syncBindings.some(b => b.property === 'files');
|
|
163
|
+
|
|
164
|
+
// === 3. BUILD: Create DOM elements ===
|
|
223
165
|
const wrapper = document.createElement('div');
|
|
224
166
|
wrapper.className = 'jux-fileupload';
|
|
225
167
|
wrapper.id = this._id;
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
|
|
168
|
+
if (className) wrapper.className += ` ${className}`;
|
|
169
|
+
if (style) wrapper.setAttribute('style', style);
|
|
170
|
+
|
|
171
|
+
if (label) {
|
|
172
|
+
const labelEl = document.createElement('label');
|
|
173
|
+
labelEl.className = 'jux-fileupload-label';
|
|
174
|
+
labelEl.textContent = label;
|
|
175
|
+
wrapper.appendChild(labelEl);
|
|
229
176
|
}
|
|
230
177
|
|
|
231
|
-
if (style) {
|
|
232
|
-
wrapper.setAttribute('style', style);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Hidden file input
|
|
236
178
|
const input = document.createElement('input');
|
|
237
179
|
input.type = 'file';
|
|
238
180
|
input.className = 'jux-fileupload-input';
|
|
239
181
|
input.id = `${this._id}-input`;
|
|
240
|
-
input.
|
|
241
|
-
input.multiple = multiple;
|
|
182
|
+
input.name = name;
|
|
242
183
|
input.disabled = disabled;
|
|
184
|
+
if (accept) input.accept = accept;
|
|
185
|
+
if (multiple) input.multiple = multiple;
|
|
243
186
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
187
|
+
const button = document.createElement('button');
|
|
188
|
+
button.type = 'button';
|
|
189
|
+
button.className = 'jux-fileupload-button';
|
|
190
|
+
button.id = `${this._id}-button`;
|
|
191
|
+
button.textContent = 'Choose File(s)';
|
|
192
|
+
button.disabled = disabled;
|
|
250
193
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
194
|
+
const fileList = document.createElement('div');
|
|
195
|
+
fileList.className = 'jux-fileupload-list';
|
|
196
|
+
fileList.id = `${this._id}-list`;
|
|
254
197
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
198
|
+
wrapper.appendChild(button);
|
|
199
|
+
wrapper.appendChild(input);
|
|
200
|
+
wrapper.appendChild(fileList);
|
|
258
201
|
|
|
259
|
-
|
|
260
|
-
textEl.className = 'jux-fileupload-text';
|
|
261
|
-
textEl.textContent = text;
|
|
202
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
262
203
|
|
|
263
|
-
|
|
264
|
-
|
|
204
|
+
// Button click triggers file input
|
|
205
|
+
button.addEventListener('click', () => input.click());
|
|
265
206
|
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
input.
|
|
270
|
-
|
|
271
|
-
|
|
207
|
+
// Default file change behavior (only if NOT using sync)
|
|
208
|
+
if (!hasFilesSync) {
|
|
209
|
+
input.addEventListener('change', () => {
|
|
210
|
+
this.state.files = Array.from(input.files || []);
|
|
211
|
+
this._updateFileList(fileList, this.state.files);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
272
214
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!disabled) {
|
|
277
|
-
dropzone.classList.add('jux-fileupload-dropzone-active');
|
|
278
|
-
textEl.textContent = dragText;
|
|
279
|
-
}
|
|
215
|
+
// Wire custom bindings from .bind() calls
|
|
216
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
217
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
280
218
|
});
|
|
281
219
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
220
|
+
// Wire sync bindings from .sync() calls
|
|
221
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
222
|
+
if (property === 'files') {
|
|
223
|
+
const transformToState = toState || ((v: any) => v);
|
|
224
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
225
|
+
|
|
226
|
+
let isUpdating = false;
|
|
227
|
+
|
|
228
|
+
// State → Component
|
|
229
|
+
stateObj.subscribe((val: any) => {
|
|
230
|
+
if (isUpdating) return;
|
|
231
|
+
const transformed = transformToComponent(val);
|
|
232
|
+
this.state.files = transformed;
|
|
233
|
+
this._updateFileList(fileList, transformed);
|
|
234
|
+
});
|
|
286
235
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
236
|
+
// Component → State
|
|
237
|
+
input.addEventListener('change', () => {
|
|
238
|
+
if (isUpdating) return;
|
|
239
|
+
isUpdating = true;
|
|
291
240
|
|
|
292
|
-
|
|
293
|
-
|
|
241
|
+
const files = Array.from(input.files || []);
|
|
242
|
+
this.state.files = files;
|
|
243
|
+
this._updateFileList(fileList, files);
|
|
244
|
+
|
|
245
|
+
const transformed = transformToState(files);
|
|
246
|
+
stateObj.set(transformed);
|
|
247
|
+
|
|
248
|
+
setTimeout(() => { isUpdating = false; }, 0);
|
|
249
|
+
});
|
|
294
250
|
}
|
|
295
251
|
});
|
|
296
252
|
|
|
297
|
-
//
|
|
298
|
-
const fileList = document.createElement('div');
|
|
299
|
-
fileList.className = 'jux-fileupload-list';
|
|
300
|
-
fileList.id = `${this._id}-list`;
|
|
301
|
-
|
|
302
|
-
wrapper.appendChild(input);
|
|
303
|
-
wrapper.appendChild(dropzone);
|
|
304
|
-
wrapper.appendChild(fileList);
|
|
253
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
305
254
|
container.appendChild(wrapper);
|
|
306
255
|
return this;
|
|
307
256
|
}
|
|
308
257
|
|
|
258
|
+
private _updateFileList(fileList: HTMLElement, files: File[]): void {
|
|
259
|
+
fileList.innerHTML = '';
|
|
260
|
+
|
|
261
|
+
if (files.length === 0) {
|
|
262
|
+
fileList.textContent = 'No files selected';
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
files.forEach(file => {
|
|
267
|
+
const fileItem = document.createElement('div');
|
|
268
|
+
fileItem.className = 'jux-fileupload-item';
|
|
269
|
+
fileItem.textContent = `${file.name} (${this._formatFileSize(file.size)})`;
|
|
270
|
+
fileList.appendChild(fileItem);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private _formatFileSize(bytes: number): string {
|
|
275
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
276
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
277
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
278
|
+
}
|
|
279
|
+
|
|
309
280
|
renderTo(juxComponent: any): this {
|
|
310
281
|
if (!juxComponent?._id) {
|
|
311
282
|
throw new Error('FileUpload.renderTo: Invalid component');
|