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,319 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FileUpload component options
|
|
5
|
+
*/
|
|
6
|
+
export interface FileUploadOptions {
|
|
7
|
+
accept?: string;
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
maxSize?: number;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
text?: string;
|
|
12
|
+
dragText?: string;
|
|
13
|
+
onChange?: (files: FileList) => void;
|
|
14
|
+
onError?: (error: string) => void;
|
|
15
|
+
style?: string;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* FileUpload component state
|
|
21
|
+
*/
|
|
22
|
+
type FileUploadState = {
|
|
23
|
+
accept: string;
|
|
24
|
+
multiple: boolean;
|
|
25
|
+
maxSize: number;
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
text: string;
|
|
28
|
+
dragText: string;
|
|
29
|
+
style: string;
|
|
30
|
+
class: string;
|
|
31
|
+
files: File[];
|
|
32
|
+
};
|
|
33
|
+
|
|
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
|
+
export class FileUpload {
|
|
48
|
+
state: FileUploadState;
|
|
49
|
+
container: HTMLElement | null = null;
|
|
50
|
+
_id: string;
|
|
51
|
+
id: string;
|
|
52
|
+
private _onChange?: (files: FileList) => void;
|
|
53
|
+
private _onError?: (error: string) => void;
|
|
54
|
+
|
|
55
|
+
constructor(id: string, options: FileUploadOptions = {}) {
|
|
56
|
+
this._id = id;
|
|
57
|
+
this.id = id;
|
|
58
|
+
this._onChange = options.onChange;
|
|
59
|
+
this._onError = options.onError;
|
|
60
|
+
|
|
61
|
+
this.state = {
|
|
62
|
+
accept: options.accept ?? '*',
|
|
63
|
+
multiple: options.multiple ?? false,
|
|
64
|
+
maxSize: options.maxSize ?? 10485760, // 10MB default
|
|
65
|
+
disabled: options.disabled ?? false,
|
|
66
|
+
text: options.text ?? 'Click to upload or drag and drop',
|
|
67
|
+
dragText: options.dragText ?? 'Drop files here',
|
|
68
|
+
style: options.style ?? '',
|
|
69
|
+
class: options.class ?? '',
|
|
70
|
+
files: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* -------------------------
|
|
75
|
+
* Fluent API
|
|
76
|
+
* ------------------------- */
|
|
77
|
+
|
|
78
|
+
accept(value: string): this {
|
|
79
|
+
this.state.accept = value;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
multiple(value: boolean): this {
|
|
84
|
+
this.state.multiple = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
maxSize(value: number): this {
|
|
89
|
+
this.state.maxSize = value;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
disabled(value: boolean): this {
|
|
94
|
+
this.state.disabled = value;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
text(value: string): this {
|
|
99
|
+
this.state.text = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dragText(value: string): this {
|
|
104
|
+
this.state.dragText = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
style(value: string): this {
|
|
109
|
+
this.state.style = value;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class(value: string): this {
|
|
114
|
+
this.state.class = value;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onChange(handler: (files: FileList) => void): this {
|
|
119
|
+
this._onChange = handler;
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onError(handler: (error: string) => void): this {
|
|
124
|
+
this._onError = handler;
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* -------------------------
|
|
129
|
+
* Helpers
|
|
130
|
+
* ------------------------- */
|
|
131
|
+
|
|
132
|
+
private _validateFiles(files: FileList): boolean {
|
|
133
|
+
for (let i = 0; i < files.length; i++) {
|
|
134
|
+
const file = files[i];
|
|
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
|
+
}
|
|
146
|
+
|
|
147
|
+
private _handleFiles(files: FileList): void {
|
|
148
|
+
if (this._validateFiles(files)) {
|
|
149
|
+
this.state.files = Array.from(files);
|
|
150
|
+
this._updateFileList();
|
|
151
|
+
if (this._onChange) {
|
|
152
|
+
this._onChange(files);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
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
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getFiles(): File[] {
|
|
191
|
+
return this.state.files;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
clear(): void {
|
|
195
|
+
this.state.files = [];
|
|
196
|
+
this._updateFileList();
|
|
197
|
+
const input = document.getElementById(`${this._id}-input`) as HTMLInputElement;
|
|
198
|
+
if (input) {
|
|
199
|
+
input.value = '';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* -------------------------
|
|
204
|
+
* Render
|
|
205
|
+
* ------------------------- */
|
|
206
|
+
|
|
207
|
+
render(targetId?: string): this {
|
|
208
|
+
let container: HTMLElement;
|
|
209
|
+
|
|
210
|
+
if (targetId) {
|
|
211
|
+
const target = document.querySelector(targetId);
|
|
212
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
213
|
+
throw new Error(`FileUpload: Target element "${targetId}" not found`);
|
|
214
|
+
}
|
|
215
|
+
container = target;
|
|
216
|
+
} else {
|
|
217
|
+
container = getOrCreateContainer(this._id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.container = container;
|
|
221
|
+
const { accept, multiple, disabled, text, dragText, style, class: className } = this.state;
|
|
222
|
+
|
|
223
|
+
const wrapper = document.createElement('div');
|
|
224
|
+
wrapper.className = 'jux-fileupload';
|
|
225
|
+
wrapper.id = this._id;
|
|
226
|
+
|
|
227
|
+
if (className) {
|
|
228
|
+
wrapper.className += ` ${className}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (style) {
|
|
232
|
+
wrapper.setAttribute('style', style);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Hidden file input
|
|
236
|
+
const input = document.createElement('input');
|
|
237
|
+
input.type = 'file';
|
|
238
|
+
input.className = 'jux-fileupload-input';
|
|
239
|
+
input.id = `${this._id}-input`;
|
|
240
|
+
input.accept = accept;
|
|
241
|
+
input.multiple = multiple;
|
|
242
|
+
input.disabled = disabled;
|
|
243
|
+
|
|
244
|
+
input.addEventListener('change', (e) => {
|
|
245
|
+
const target = e.target as HTMLInputElement;
|
|
246
|
+
if (target.files && target.files.length > 0) {
|
|
247
|
+
this._handleFiles(target.files);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Drop zone
|
|
252
|
+
const dropzone = document.createElement('div');
|
|
253
|
+
dropzone.className = 'jux-fileupload-dropzone';
|
|
254
|
+
|
|
255
|
+
const icon = document.createElement('div');
|
|
256
|
+
icon.className = 'jux-fileupload-icon';
|
|
257
|
+
icon.textContent = '📁';
|
|
258
|
+
|
|
259
|
+
const textEl = document.createElement('div');
|
|
260
|
+
textEl.className = 'jux-fileupload-text';
|
|
261
|
+
textEl.textContent = text;
|
|
262
|
+
|
|
263
|
+
dropzone.appendChild(icon);
|
|
264
|
+
dropzone.appendChild(textEl);
|
|
265
|
+
|
|
266
|
+
// Click to open file dialog
|
|
267
|
+
dropzone.addEventListener('click', () => {
|
|
268
|
+
if (!disabled) {
|
|
269
|
+
input.click();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Drag & drop
|
|
274
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
if (!disabled) {
|
|
277
|
+
dropzone.classList.add('jux-fileupload-dropzone-active');
|
|
278
|
+
textEl.textContent = dragText;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
dropzone.addEventListener('dragleave', () => {
|
|
283
|
+
dropzone.classList.remove('jux-fileupload-dropzone-active');
|
|
284
|
+
textEl.textContent = text;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
dropzone.addEventListener('drop', (e) => {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
dropzone.classList.remove('jux-fileupload-dropzone-active');
|
|
290
|
+
textEl.textContent = text;
|
|
291
|
+
|
|
292
|
+
if (!disabled && e.dataTransfer?.files) {
|
|
293
|
+
this._handleFiles(e.dataTransfer.files);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// File list
|
|
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);
|
|
305
|
+
container.appendChild(wrapper);
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
renderTo(juxComponent: any): this {
|
|
310
|
+
if (!juxComponent?._id) {
|
|
311
|
+
throw new Error('FileUpload.renderTo: Invalid component');
|
|
312
|
+
}
|
|
313
|
+
return this.render(`#${juxComponent._id}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function fileupload(id: string, options: FileUploadOptions = {}): FileUpload {
|
|
318
|
+
return new FileUpload(id, options);
|
|
319
|
+
}
|
package/lib/components/footer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Footer component options
|
|
@@ -7,6 +7,8 @@ export interface FooterOptions {
|
|
|
7
7
|
content?: string;
|
|
8
8
|
copyright?: string;
|
|
9
9
|
links?: Array<{ label: string; href: string }>;
|
|
10
|
+
class?: string;
|
|
11
|
+
style?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -16,6 +18,8 @@ type FooterState = {
|
|
|
16
18
|
content: string;
|
|
17
19
|
copyright: string;
|
|
18
20
|
links: Array<{ label: string; href: string }>;
|
|
21
|
+
class: string;
|
|
22
|
+
style: string;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -31,19 +35,23 @@ type FooterState = {
|
|
|
31
35
|
* });
|
|
32
36
|
* footer.render('#appfooter');
|
|
33
37
|
*/
|
|
34
|
-
export class Footer
|
|
35
|
-
state
|
|
38
|
+
export class Footer {
|
|
39
|
+
state: FooterState;
|
|
36
40
|
container: HTMLElement | null = null;
|
|
41
|
+
_id: string;
|
|
42
|
+
id: string;
|
|
37
43
|
|
|
38
|
-
constructor(
|
|
39
|
-
|
|
40
|
-
this.
|
|
44
|
+
constructor(id: string, options: FooterOptions = {}) {
|
|
45
|
+
this._id = id;
|
|
46
|
+
this.id = id;
|
|
41
47
|
|
|
42
|
-
this.state =
|
|
48
|
+
this.state = {
|
|
43
49
|
content: options.content ?? '',
|
|
44
50
|
copyright: options.copyright ?? '',
|
|
45
|
-
links: options.links ?? []
|
|
46
|
-
|
|
51
|
+
links: options.links ?? [],
|
|
52
|
+
class: options.class ?? '',
|
|
53
|
+
style: options.style ?? ''
|
|
54
|
+
};
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/* -------------------------
|
|
@@ -65,6 +73,16 @@ export class Footer extends Reactive {
|
|
|
65
73
|
return this;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
class(value: string): this {
|
|
77
|
+
this.state.class = value;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
style(value: string): this {
|
|
82
|
+
this.state.style = value;
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
/* -------------------------
|
|
69
87
|
* Render
|
|
70
88
|
* ------------------------- */
|
|
@@ -79,15 +97,16 @@ export class Footer extends Reactive {
|
|
|
79
97
|
}
|
|
80
98
|
container = target;
|
|
81
99
|
} else {
|
|
82
|
-
container = getOrCreateContainer(this.
|
|
100
|
+
container = getOrCreateContainer(this._id);
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
this.container = container;
|
|
86
|
-
const { content, copyright, links } = this.state;
|
|
104
|
+
const { content, copyright, links, class: className, style } = this.state;
|
|
87
105
|
|
|
88
106
|
const footer = document.createElement('footer');
|
|
89
|
-
footer.className =
|
|
90
|
-
footer.
|
|
107
|
+
footer.className = `jux-footer ${className}`.trim();
|
|
108
|
+
footer.style.cssText = style;
|
|
109
|
+
footer.id = this._id;
|
|
91
110
|
|
|
92
111
|
if (content) {
|
|
93
112
|
const contentEl = document.createElement('div');
|
|
@@ -130,17 +149,17 @@ export class Footer extends Reactive {
|
|
|
130
149
|
throw new Error('Footer.renderTo: Invalid component - not an object');
|
|
131
150
|
}
|
|
132
151
|
|
|
133
|
-
if (!juxComponent.
|
|
134
|
-
throw new Error('Footer.renderTo: Invalid component - missing
|
|
152
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
153
|
+
throw new Error('Footer.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
return this.render(`#${juxComponent.
|
|
156
|
+
return this.render(`#${juxComponent._id}`);
|
|
138
157
|
}
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
142
161
|
* Factory helper
|
|
143
162
|
*/
|
|
144
|
-
export function footer(
|
|
145
|
-
return new Footer(
|
|
163
|
+
export function footer(id: string, options: FooterOptions = {}): Footer {
|
|
164
|
+
return new Footer(id, options);
|
|
146
165
|
}
|
package/lib/components/header.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Header component options
|
|
@@ -8,6 +8,8 @@ export interface HeaderOptions {
|
|
|
8
8
|
logo?: string;
|
|
9
9
|
navigation?: Array<{ label: string; href: string }>;
|
|
10
10
|
sticky?: boolean;
|
|
11
|
+
style?: string;
|
|
12
|
+
class?: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -18,6 +20,8 @@ type HeaderState = {
|
|
|
18
20
|
logo: string;
|
|
19
21
|
navigation: Array<{ label: string; href: string }>;
|
|
20
22
|
sticky: boolean;
|
|
23
|
+
style: string;
|
|
24
|
+
class: string;
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -33,20 +37,24 @@ type HeaderState = {
|
|
|
33
37
|
* });
|
|
34
38
|
* header.render('#appheader');
|
|
35
39
|
*/
|
|
36
|
-
export class Header
|
|
37
|
-
state
|
|
40
|
+
export class Header {
|
|
41
|
+
state: HeaderState;
|
|
38
42
|
container: HTMLElement | null = null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
43
|
+
_id: string;
|
|
44
|
+
id: string;
|
|
45
|
+
|
|
46
|
+
constructor(id: string, options: HeaderOptions = {}) {
|
|
47
|
+
this._id = id;
|
|
48
|
+
this.id = id;
|
|
49
|
+
|
|
50
|
+
this.state = {
|
|
45
51
|
title: options.title ?? '',
|
|
46
52
|
logo: options.logo ?? '',
|
|
47
53
|
navigation: options.navigation ?? [],
|
|
48
|
-
sticky: options.sticky ?? true
|
|
49
|
-
|
|
54
|
+
sticky: options.sticky ?? true,
|
|
55
|
+
style: options.style ?? '',
|
|
56
|
+
class: options.class ?? ''
|
|
57
|
+
};
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
/* -------------------------
|
|
@@ -73,13 +81,22 @@ export class Header extends Reactive {
|
|
|
73
81
|
return this;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
class(value: string): this {
|
|
85
|
+
this.state.class = value;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
style(value: string): this {
|
|
90
|
+
this.state.style = value;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
76
93
|
/* -------------------------
|
|
77
94
|
* Render
|
|
78
95
|
* ------------------------- */
|
|
79
96
|
|
|
80
97
|
render(targetId?: string): this {
|
|
81
98
|
let container: HTMLElement;
|
|
82
|
-
|
|
99
|
+
|
|
83
100
|
if (targetId) {
|
|
84
101
|
const target = document.querySelector(targetId);
|
|
85
102
|
if (!target || !(target instanceof HTMLElement)) {
|
|
@@ -87,47 +104,50 @@ export class Header extends Reactive {
|
|
|
87
104
|
}
|
|
88
105
|
container = target;
|
|
89
106
|
} else {
|
|
90
|
-
container = getOrCreateContainer(this.
|
|
107
|
+
container = getOrCreateContainer(this._id);
|
|
91
108
|
}
|
|
92
|
-
|
|
109
|
+
|
|
93
110
|
this.container = container;
|
|
94
|
-
const { title, logo, navigation, sticky } = this.state;
|
|
95
|
-
|
|
111
|
+
const { title, logo, navigation, sticky, style, class: className } = this.state;
|
|
112
|
+
|
|
96
113
|
const header = document.createElement('header');
|
|
97
114
|
header.className = 'jux-header';
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
|
|
116
|
+
header.id = this._id;
|
|
117
|
+
header.style.cssText = style;
|
|
118
|
+
header.className = `jux-header ${className}`.trim();
|
|
119
|
+
|
|
100
120
|
if (sticky) {
|
|
101
121
|
header.classList.add('jux-header-sticky');
|
|
102
122
|
}
|
|
103
|
-
|
|
123
|
+
|
|
104
124
|
// Logo section
|
|
105
125
|
if (logo || title) {
|
|
106
126
|
const logoSection = document.createElement('div');
|
|
107
127
|
logoSection.className = 'jux-header-logo';
|
|
108
|
-
|
|
128
|
+
|
|
109
129
|
if (logo) {
|
|
110
130
|
const logoImg = document.createElement('img');
|
|
111
131
|
logoImg.src = logo;
|
|
112
132
|
logoImg.alt = title || 'Logo';
|
|
113
133
|
logoSection.appendChild(logoImg);
|
|
114
134
|
}
|
|
115
|
-
|
|
135
|
+
|
|
116
136
|
if (title) {
|
|
117
137
|
const titleEl = document.createElement('span');
|
|
118
138
|
titleEl.className = 'jux-header-title';
|
|
119
139
|
titleEl.textContent = title;
|
|
120
140
|
logoSection.appendChild(titleEl);
|
|
121
141
|
}
|
|
122
|
-
|
|
142
|
+
|
|
123
143
|
header.appendChild(logoSection);
|
|
124
144
|
}
|
|
125
|
-
|
|
145
|
+
|
|
126
146
|
// Navigation
|
|
127
147
|
if (navigation.length > 0) {
|
|
128
148
|
const nav = document.createElement('nav');
|
|
129
149
|
nav.className = 'jux-header-nav';
|
|
130
|
-
|
|
150
|
+
|
|
131
151
|
navigation.forEach(item => {
|
|
132
152
|
const link = document.createElement('a');
|
|
133
153
|
link.className = 'jux-header-nav-item';
|
|
@@ -135,10 +155,10 @@ export class Header extends Reactive {
|
|
|
135
155
|
link.textContent = item.label;
|
|
136
156
|
nav.appendChild(link);
|
|
137
157
|
});
|
|
138
|
-
|
|
158
|
+
|
|
139
159
|
header.appendChild(nav);
|
|
140
160
|
}
|
|
141
|
-
|
|
161
|
+
|
|
142
162
|
container.appendChild(header);
|
|
143
163
|
return this;
|
|
144
164
|
}
|
|
@@ -150,18 +170,18 @@ export class Header extends Reactive {
|
|
|
150
170
|
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
151
171
|
throw new Error('Header.renderTo: Invalid component - not an object');
|
|
152
172
|
}
|
|
153
|
-
|
|
154
|
-
if (!juxComponent.
|
|
155
|
-
throw new Error('Header.renderTo: Invalid component - missing
|
|
173
|
+
|
|
174
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
175
|
+
throw new Error('Header.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
156
176
|
}
|
|
157
|
-
|
|
158
|
-
return this.render(`#${juxComponent.
|
|
177
|
+
|
|
178
|
+
return this.render(`#${juxComponent._id}`);
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
181
|
|
|
162
182
|
/**
|
|
163
183
|
* Factory helper
|
|
164
184
|
*/
|
|
165
|
-
export function header(
|
|
166
|
-
return new Header(
|
|
185
|
+
export function header(id: string, options: HeaderOptions = {}): Header {
|
|
186
|
+
return new Header(id, options);
|
|
167
187
|
}
|