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,5 +1,22 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
import { State } from '../reactivity/state.js';
|
|
3
|
+
import {
|
|
4
|
+
ChartDataPoint,
|
|
5
|
+
DoughnutChartOptions as DoughnutChartOptionsBase,
|
|
6
|
+
DoughnutChartState as DoughnutChartStateBase,
|
|
7
|
+
ChartTheme,
|
|
8
|
+
ChartStyleMode,
|
|
9
|
+
LegendOrientation,
|
|
10
|
+
ChartPropertyMapping,
|
|
11
|
+
ChartStateObject
|
|
12
|
+
} from './chart-types.js';
|
|
13
|
+
import {
|
|
14
|
+
lightenColor,
|
|
15
|
+
getThemeConfig,
|
|
16
|
+
createLegend,
|
|
17
|
+
createDataTable,
|
|
18
|
+
applyThemeStyles
|
|
19
|
+
} from './chart-utils.js';
|
|
3
20
|
import {
|
|
4
21
|
googleTheme,
|
|
5
22
|
seriesaTheme,
|
|
@@ -658,7 +675,7 @@ export class DoughnutChart {
|
|
|
658
675
|
path.setAttribute('stroke-dasharray', '8,4');
|
|
659
676
|
} else if (styleMode === 'glow') {
|
|
660
677
|
path.setAttribute('fill', color);
|
|
661
|
-
|
|
678
|
+
|
|
662
679
|
const filterId = `glow-slice-${this._id}-${index}`;
|
|
663
680
|
const defs = svg.querySelector('defs') || document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
664
681
|
if (!svg.querySelector('defs')) {
|
|
@@ -701,24 +718,24 @@ export class DoughnutChart {
|
|
|
701
718
|
if (showDataLabels) {
|
|
702
719
|
const middleAngle = startAngle + (sliceAngle / 2);
|
|
703
720
|
const middleRad = (middleAngle * Math.PI) / 180;
|
|
704
|
-
|
|
721
|
+
|
|
705
722
|
// Line start (from outer edge)
|
|
706
723
|
const lineStartRadius = radius + 5;
|
|
707
724
|
const lineStartX = centerX + lineStartRadius * Math.cos(middleRad);
|
|
708
725
|
const lineStartY = centerY + lineStartRadius * Math.sin(middleRad);
|
|
709
|
-
|
|
726
|
+
|
|
710
727
|
// Bubble position (along the line)
|
|
711
728
|
const bubbleRadius = radius + 45;
|
|
712
729
|
const bubbleX = centerX + bubbleRadius * Math.cos(middleRad);
|
|
713
730
|
const bubbleY = centerY + bubbleRadius * Math.sin(middleRad);
|
|
714
|
-
|
|
731
|
+
|
|
715
732
|
// Label position (beyond the bubble)
|
|
716
733
|
const labelDistance = radius + 80;
|
|
717
734
|
const externalLabelX = centerX + labelDistance * Math.cos(middleRad);
|
|
718
735
|
const externalLabelY = centerY + labelDistance * Math.sin(middleRad);
|
|
719
736
|
|
|
720
737
|
const lineGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
721
|
-
|
|
738
|
+
|
|
722
739
|
if (animate) {
|
|
723
740
|
lineGroup.classList.add('jux-label-animated');
|
|
724
741
|
lineGroup.style.animationDelay = `${index * 150 + animationDuration + 100}ms`;
|
|
@@ -800,7 +817,7 @@ export class DoughnutChart {
|
|
|
800
817
|
const externalLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
|
801
818
|
externalLabel.setAttribute('x', externalLabelX.toString());
|
|
802
819
|
externalLabel.setAttribute('y', externalLabelY.toString());
|
|
803
|
-
|
|
820
|
+
|
|
804
821
|
// Anchor based on which side of the circle
|
|
805
822
|
const anchor = middleAngle > -90 && middleAngle < 90 ? 'start' : 'end';
|
|
806
823
|
externalLabel.setAttribute('text-anchor', anchor);
|
|
@@ -818,12 +835,12 @@ export class DoughnutChart {
|
|
|
818
835
|
// Add hover effect
|
|
819
836
|
path.style.cursor = 'pointer';
|
|
820
837
|
path.style.transition = 'transform 0.2s, opacity 0.2s';
|
|
821
|
-
|
|
838
|
+
|
|
822
839
|
path.addEventListener('mouseenter', () => {
|
|
823
840
|
path.style.transform = 'scale(1.05)';
|
|
824
841
|
path.style.opacity = '0.9';
|
|
825
842
|
});
|
|
826
|
-
|
|
843
|
+
|
|
827
844
|
path.addEventListener('mouseleave', () => {
|
|
828
845
|
path.style.transform = 'scale(1)';
|
|
829
846
|
path.style.opacity = '1';
|
|
@@ -843,6 +860,103 @@ export class DoughnutChart {
|
|
|
843
860
|
// Not used for doughnut chart
|
|
844
861
|
}
|
|
845
862
|
|
|
863
|
+
/* -------------------------
|
|
864
|
+
* Reactivity Support
|
|
865
|
+
* ------------------------- */
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Sync a single property to a state object
|
|
869
|
+
*/
|
|
870
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
871
|
+
const transform = toComponent || ((v: any) => v);
|
|
872
|
+
|
|
873
|
+
stateObj.subscribe((val: any) => {
|
|
874
|
+
const transformed = transform(val);
|
|
875
|
+
|
|
876
|
+
// Map property to correct method
|
|
877
|
+
switch (property) {
|
|
878
|
+
case 'data': this.data(transformed); break;
|
|
879
|
+
case 'title': this.title(transformed); break;
|
|
880
|
+
case 'subtitle': this.subtitle(transformed); break;
|
|
881
|
+
case 'width': this.width(transformed); break;
|
|
882
|
+
case 'height': this.height(transformed); break;
|
|
883
|
+
case 'theme': this.theme(transformed); break;
|
|
884
|
+
case 'styleMode': this.styleMode(transformed); break;
|
|
885
|
+
case 'borderRadius': this.borderRadius(transformed); break;
|
|
886
|
+
case 'showLegend': this.showLegend(transformed); break;
|
|
887
|
+
case 'showDataLabels': this.showDataLabels(transformed); break;
|
|
888
|
+
case 'showDataTable': this.showDataTable(transformed); break;
|
|
889
|
+
case 'animate': this.animate(transformed); break;
|
|
890
|
+
case 'animationDuration': this.animationDuration(transformed); break;
|
|
891
|
+
case 'legendOrientation': this.legendOrientation(transformed); break;
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
return this;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Sync multiple properties from a state object
|
|
900
|
+
*/
|
|
901
|
+
syncState(stateObject: ChartStateObject, mapping?: ChartPropertyMapping): this {
|
|
902
|
+
// Default mapping: camelCase state names to method names
|
|
903
|
+
const defaultMapping: ChartPropertyMapping = {
|
|
904
|
+
chartType: 'type', // Not used in doughnut chart, ignored
|
|
905
|
+
chartTheme: 'theme',
|
|
906
|
+
chartStyleMode: 'styleMode',
|
|
907
|
+
borderRadius: 'borderRadius',
|
|
908
|
+
chartTitle: 'title',
|
|
909
|
+
chartWidth: 'width',
|
|
910
|
+
chartHeight: 'height',
|
|
911
|
+
showLegend: 'showLegend',
|
|
912
|
+
showDataTable: 'showDataTable',
|
|
913
|
+
showDataLabels: 'showDataLabels',
|
|
914
|
+
animate: 'animate',
|
|
915
|
+
animationDuration: 'animationDuration',
|
|
916
|
+
legendOrientation: 'legendOrientation',
|
|
917
|
+
// Note: Doughnut charts don't use these, but we handle them gracefully
|
|
918
|
+
showTicksX: null,
|
|
919
|
+
showTicksY: null,
|
|
920
|
+
chartOrientation: null,
|
|
921
|
+
chartDirection: null
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const finalMapping = { ...defaultMapping, ...mapping };
|
|
925
|
+
|
|
926
|
+
// Iterate through state object and bind each property
|
|
927
|
+
Object.entries(stateObject).forEach(([key, stateObj]) => {
|
|
928
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const methodOrFunction = finalMapping[key];
|
|
933
|
+
|
|
934
|
+
// Skip null mappings (properties not applicable to this chart type)
|
|
935
|
+
if (methodOrFunction === null || !methodOrFunction) {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (typeof methodOrFunction === 'function') {
|
|
940
|
+
// Custom function mapping
|
|
941
|
+
stateObj.subscribe(methodOrFunction);
|
|
942
|
+
} else {
|
|
943
|
+
// String mapping to method name
|
|
944
|
+
const methodName = methodOrFunction as keyof this;
|
|
945
|
+
const method = this[methodName];
|
|
946
|
+
|
|
947
|
+
if (typeof method !== 'function') {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
stateObj.subscribe((val: any) => {
|
|
952
|
+
(method as Function).call(this, val);
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
return this;
|
|
958
|
+
}
|
|
959
|
+
|
|
846
960
|
/* -------------------------
|
|
847
961
|
* Legend and Data Table
|
|
848
962
|
* ------------------------- */
|
|
@@ -968,59 +1082,13 @@ export class DoughnutChart {
|
|
|
968
1082
|
}
|
|
969
1083
|
|
|
970
1084
|
private _applyTheme(themeName: string): void {
|
|
971
|
-
const
|
|
972
|
-
google: googleTheme,
|
|
973
|
-
seriesa: seriesaTheme,
|
|
974
|
-
hr: hrTheme,
|
|
975
|
-
figma: figmaTheme,
|
|
976
|
-
notion: notionTheme,
|
|
977
|
-
chalk: chalkTheme,
|
|
978
|
-
mint: mintTheme
|
|
979
|
-
};
|
|
980
|
-
|
|
981
|
-
const theme = themes[themeName];
|
|
1085
|
+
const theme = getThemeConfig(themeName as ChartTheme);
|
|
982
1086
|
if (!theme) return;
|
|
983
1087
|
|
|
984
1088
|
// Apply colors
|
|
985
1089
|
this.state.colors = theme.colors;
|
|
986
1090
|
|
|
987
|
-
|
|
988
|
-
const baseStyleId = 'jux-doughnutchart-base-styles';
|
|
989
|
-
if (!document.getElementById(baseStyleId)) {
|
|
990
|
-
const style = document.createElement('style');
|
|
991
|
-
style.id = baseStyleId;
|
|
992
|
-
style.textContent = this._getBaseStyles();
|
|
993
|
-
document.head.appendChild(style);
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
// Inject font (once per theme)
|
|
997
|
-
if (theme.font && !document.querySelector(`link[href="${theme.font}"]`)) {
|
|
998
|
-
const link = document.createElement('link');
|
|
999
|
-
link.rel = 'stylesheet';
|
|
1000
|
-
link.href = theme.font;
|
|
1001
|
-
document.head.appendChild(link);
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Apply theme-specific styles
|
|
1005
|
-
const styleId = `jux-doughnutchart-theme-${themeName}`;
|
|
1006
|
-
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
|
1007
|
-
|
|
1008
|
-
if (!styleElement) {
|
|
1009
|
-
styleElement = document.createElement('style');
|
|
1010
|
-
styleElement.id = styleId;
|
|
1011
|
-
document.head.appendChild(styleElement);
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Generate CSS with theme variables
|
|
1015
|
-
const variablesCSS = Object.entries(theme.variables)
|
|
1016
|
-
.map(([key, value]) => ` ${key}: ${value};`)
|
|
1017
|
-
.join('\n');
|
|
1018
|
-
|
|
1019
|
-
styleElement.textContent = `
|
|
1020
|
-
.jux-doughnutchart.theme-${themeName} {
|
|
1021
|
-
${variablesCSS}
|
|
1022
|
-
}
|
|
1023
|
-
`;
|
|
1091
|
+
applyThemeStyles(themeName as ChartTheme, 'jux-doughnutchart', this._getBaseStyles());
|
|
1024
1092
|
}
|
|
1025
1093
|
|
|
1026
1094
|
private _applyThemeToWrapper(wrapper: HTMLElement): void {
|
|
@@ -1131,7 +1199,7 @@ ${variablesCSS}
|
|
|
1131
1199
|
.jux-doughnutchart-svg {
|
|
1132
1200
|
font-family: inherit;
|
|
1133
1201
|
}
|
|
1134
|
-
`;
|
|
1202
|
+
`;
|
|
1135
1203
|
}
|
|
1136
1204
|
|
|
1137
1205
|
render(targetId?: string | HTMLElement): this {
|
|
@@ -1,77 +1,75 @@
|
|
|
1
1
|
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
import { State } from '../reactivity/state.js';
|
|
3
|
+
import { renderIcon } from './icons.js';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
* Dropdown menu item
|
|
5
|
-
*/
|
|
6
|
-
export interface DropdownItem {
|
|
5
|
+
export interface DropdownOption {
|
|
7
6
|
label: string;
|
|
8
|
-
value
|
|
9
|
-
icon?: string;
|
|
10
|
-
click?: () => void;
|
|
7
|
+
value: string;
|
|
11
8
|
disabled?: boolean;
|
|
12
|
-
divider?: boolean;
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
/**
|
|
16
|
-
* Dropdown component options
|
|
17
|
-
*/
|
|
18
11
|
export interface DropdownOptions {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
options?: DropdownOption[];
|
|
13
|
+
value?: string;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
disabled?: boolean;
|
|
22
17
|
style?: string;
|
|
23
18
|
class?: string;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
export interface DropdownItem {
|
|
22
|
+
label: string;
|
|
23
|
+
value?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
onClick?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
29
28
|
type DropdownState = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
options: DropdownOption[];
|
|
30
|
+
value: string;
|
|
31
|
+
placeholder: string;
|
|
32
|
+
label: string;
|
|
33
|
+
disabled: boolean;
|
|
33
34
|
style: string;
|
|
34
35
|
class: string;
|
|
35
36
|
isOpen: boolean;
|
|
37
|
+
items: any[];
|
|
38
|
+
trigger: string;
|
|
39
|
+
position: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
36
40
|
};
|
|
37
41
|
|
|
38
|
-
/**
|
|
39
|
-
* Dropdown Menu component - Context menus, action menus
|
|
40
|
-
*
|
|
41
|
-
* Usage:
|
|
42
|
-
* jux.dropdown('actions', {
|
|
43
|
-
* trigger: 'Actions ▾',
|
|
44
|
-
* position: 'bottom-right',
|
|
45
|
-
* items: [
|
|
46
|
-
* { label: 'Edit', icon: '✏️', click: () => console.log('Edit') },
|
|
47
|
-
* { label: 'Delete', icon: '🗑️', click: () => console.log('Delete') },
|
|
48
|
-
* { divider: true },
|
|
49
|
-
* { label: 'Archive', click: () => console.log('Archive') }
|
|
50
|
-
* ]
|
|
51
|
-
* }).render('#toolbar');
|
|
52
|
-
*
|
|
53
|
-
* // Control programmatically
|
|
54
|
-
* const dd = jux.dropdown('my-dropdown').render();
|
|
55
|
-
* dd.open();
|
|
56
|
-
* dd.close();
|
|
57
|
-
*/
|
|
58
42
|
export class Dropdown {
|
|
59
43
|
state: DropdownState;
|
|
60
44
|
container: HTMLElement | null = null;
|
|
61
45
|
_id: string;
|
|
62
46
|
id: string;
|
|
63
47
|
|
|
48
|
+
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
49
|
+
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
50
|
+
private _syncBindings: Array<{
|
|
51
|
+
property: string,
|
|
52
|
+
stateObj: State<any>,
|
|
53
|
+
toState?: Function,
|
|
54
|
+
toComponent?: Function
|
|
55
|
+
}> = [];
|
|
56
|
+
|
|
64
57
|
constructor(id: string, options: DropdownOptions = {}) {
|
|
65
58
|
this._id = id;
|
|
66
59
|
this.id = id;
|
|
67
60
|
|
|
68
61
|
this.state = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
options: options.options ?? [],
|
|
63
|
+
value: options.value ?? '',
|
|
64
|
+
placeholder: options.placeholder ?? 'Select...',
|
|
65
|
+
label: options.label ?? '',
|
|
66
|
+
disabled: options.disabled ?? false,
|
|
72
67
|
style: options.style ?? '',
|
|
73
68
|
class: options.class ?? '',
|
|
74
|
-
isOpen: false
|
|
69
|
+
isOpen: false,
|
|
70
|
+
items: [],
|
|
71
|
+
trigger: '',
|
|
72
|
+
position: 'bottom-left'
|
|
75
73
|
};
|
|
76
74
|
}
|
|
77
75
|
|
|
@@ -109,6 +107,19 @@ export class Dropdown {
|
|
|
109
107
|
return this;
|
|
110
108
|
}
|
|
111
109
|
|
|
110
|
+
bind(event: string, handler: Function): this {
|
|
111
|
+
this._bindings.push({ event, handler });
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
|
|
116
|
+
if (!stateObj || typeof stateObj.subscribe !== 'function') {
|
|
117
|
+
throw new Error(`Dropdown.sync: Expected a State object for property "${property}"`);
|
|
118
|
+
}
|
|
119
|
+
this._syncBindings.push({ property, stateObj, toState, toComponent });
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
/* -------------------------
|
|
113
124
|
* Methods
|
|
114
125
|
* ------------------------- */
|
|
@@ -142,47 +153,37 @@ export class Dropdown {
|
|
|
142
153
|
* ------------------------- */
|
|
143
154
|
|
|
144
155
|
render(targetId?: string): this {
|
|
156
|
+
// === 1. SETUP: Get or create container ===
|
|
145
157
|
let container: HTMLElement;
|
|
146
|
-
|
|
147
158
|
if (targetId) {
|
|
148
159
|
const target = document.querySelector(targetId);
|
|
149
160
|
if (!target || !(target instanceof HTMLElement)) {
|
|
150
|
-
throw new Error(`Dropdown: Target
|
|
161
|
+
throw new Error(`Dropdown: Target "${targetId}" not found`);
|
|
151
162
|
}
|
|
152
163
|
container = target;
|
|
153
164
|
} else {
|
|
154
165
|
container = getOrCreateContainer(this._id);
|
|
155
166
|
}
|
|
156
|
-
|
|
157
167
|
this.container = container;
|
|
158
|
-
const { items, trigger, position, style, class: className } = this.state;
|
|
159
168
|
|
|
160
|
-
//
|
|
169
|
+
// === 2. PREPARE: Destructure state and check sync flags ===
|
|
170
|
+
const { label, items, style, class: className } = this.state;
|
|
171
|
+
const hasItemsSync = this._syncBindings.some(b => b.property === 'items');
|
|
172
|
+
|
|
173
|
+
// === 3. BUILD: Create DOM elements ===
|
|
161
174
|
const wrapper = document.createElement('div');
|
|
162
175
|
wrapper.className = 'jux-dropdown';
|
|
163
176
|
wrapper.id = this._id;
|
|
177
|
+
if (className) wrapper.className += ` ${className}`;
|
|
178
|
+
if (style) wrapper.setAttribute('style', style);
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
const button = document.createElement('button');
|
|
181
|
+
button.className = 'jux-dropdown-button';
|
|
182
|
+
button.textContent = label;
|
|
168
183
|
|
|
169
|
-
if (style) {
|
|
170
|
-
wrapper.setAttribute('style', style);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Trigger button
|
|
174
|
-
const triggerBtn = document.createElement('button');
|
|
175
|
-
triggerBtn.className = 'jux-dropdown-trigger';
|
|
176
|
-
triggerBtn.textContent = trigger;
|
|
177
|
-
triggerBtn.addEventListener('click', (e) => {
|
|
178
|
-
e.stopPropagation();
|
|
179
|
-
this.toggle();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Menu
|
|
183
184
|
const menu = document.createElement('div');
|
|
184
|
-
menu.className =
|
|
185
|
-
menu.
|
|
185
|
+
menu.className = 'jux-dropdown-menu';
|
|
186
|
+
menu.style.display = 'none';
|
|
186
187
|
|
|
187
188
|
items.forEach(item => {
|
|
188
189
|
if (item.divider) {
|
|
@@ -190,41 +191,127 @@ export class Dropdown {
|
|
|
190
191
|
divider.className = 'jux-dropdown-divider';
|
|
191
192
|
menu.appendChild(divider);
|
|
192
193
|
} else {
|
|
193
|
-
const menuItem = document.createElement('
|
|
194
|
+
const menuItem = document.createElement('a');
|
|
194
195
|
menuItem.className = 'jux-dropdown-item';
|
|
195
|
-
|
|
196
|
+
|
|
197
|
+
if (item.href) {
|
|
198
|
+
menuItem.href = item.href;
|
|
199
|
+
}
|
|
196
200
|
|
|
197
201
|
if (item.icon) {
|
|
198
202
|
const icon = document.createElement('span');
|
|
199
|
-
icon.className = 'jux-dropdown-
|
|
200
|
-
icon.
|
|
203
|
+
icon.className = 'jux-dropdown-icon';
|
|
204
|
+
icon.appendChild(renderIcon(item.icon));
|
|
201
205
|
menuItem.appendChild(icon);
|
|
202
206
|
}
|
|
203
207
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
menuItem.appendChild(label);
|
|
208
|
+
const text = document.createElement('span');
|
|
209
|
+
text.textContent = item.label;
|
|
210
|
+
menuItem.appendChild(text);
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
if (item.click) {
|
|
213
|
+
menuItem.addEventListener('click', (e) => {
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
item.click!();
|
|
216
|
+
menu.style.display = 'none';
|
|
217
|
+
});
|
|
218
|
+
}
|
|
215
219
|
|
|
216
220
|
menu.appendChild(menuItem);
|
|
217
221
|
}
|
|
218
222
|
});
|
|
219
223
|
|
|
220
|
-
wrapper.appendChild(
|
|
224
|
+
wrapper.appendChild(button);
|
|
221
225
|
wrapper.appendChild(menu);
|
|
222
|
-
|
|
226
|
+
|
|
227
|
+
// Toggle functionality
|
|
228
|
+
button.addEventListener('click', () => {
|
|
229
|
+
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
|
230
|
+
});
|
|
223
231
|
|
|
224
232
|
// Close on outside click
|
|
225
233
|
document.addEventListener('click', (e) => {
|
|
226
234
|
if (!wrapper.contains(e.target as Node)) {
|
|
227
|
-
|
|
235
|
+
menu.style.display = 'none';
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
240
|
+
|
|
241
|
+
// Wire custom bindings from .bind() calls
|
|
242
|
+
this._bindings.forEach(({ event, handler }) => {
|
|
243
|
+
wrapper.addEventListener(event, handler as EventListener);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Wire sync bindings from .sync() calls
|
|
247
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
248
|
+
if (property === 'items') {
|
|
249
|
+
const transformToComponent = toComponent || ((v: any) => v);
|
|
250
|
+
|
|
251
|
+
stateObj.subscribe((val: any) => {
|
|
252
|
+
const transformed = transformToComponent(val);
|
|
253
|
+
this.state.items = transformed;
|
|
254
|
+
|
|
255
|
+
// Re-render menu items
|
|
256
|
+
menu.innerHTML = '';
|
|
257
|
+
transformed.forEach((item: any) => {
|
|
258
|
+
if (item.divider) {
|
|
259
|
+
const divider = document.createElement('div');
|
|
260
|
+
divider.className = 'jux-dropdown-divider';
|
|
261
|
+
menu.appendChild(divider);
|
|
262
|
+
} else {
|
|
263
|
+
const menuItem = document.createElement('a');
|
|
264
|
+
menuItem.className = 'jux-dropdown-item';
|
|
265
|
+
|
|
266
|
+
if (item.href) menuItem.href = item.href;
|
|
267
|
+
|
|
268
|
+
if (item.icon) {
|
|
269
|
+
const icon = document.createElement('span');
|
|
270
|
+
icon.className = 'jux-dropdown-icon';
|
|
271
|
+
icon.appendChild(renderIcon(item.icon));
|
|
272
|
+
menuItem.appendChild(icon);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const text = document.createElement('span');
|
|
276
|
+
text.textContent = item.label;
|
|
277
|
+
menuItem.appendChild(text);
|
|
278
|
+
|
|
279
|
+
if (item.click) {
|
|
280
|
+
menuItem.addEventListener('click', (e) => {
|
|
281
|
+
e.preventDefault();
|
|
282
|
+
item.click!();
|
|
283
|
+
menu.style.display = 'none';
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
menu.appendChild(menuItem);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
requestAnimationFrame(() => {
|
|
292
|
+
if ((window as any).lucide) {
|
|
293
|
+
(window as any).lucide.createIcons();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else if (property === 'label') {
|
|
299
|
+
const transformToComponent = toComponent || ((v: any) => String(v));
|
|
300
|
+
|
|
301
|
+
stateObj.subscribe((val: any) => {
|
|
302
|
+
const transformed = transformToComponent(val);
|
|
303
|
+
button.textContent = transformed;
|
|
304
|
+
this.state.label = transformed;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// === 5. RENDER: Append to DOM and finalize ===
|
|
310
|
+
container.appendChild(wrapper);
|
|
311
|
+
|
|
312
|
+
requestAnimationFrame(() => {
|
|
313
|
+
if ((window as any).lucide) {
|
|
314
|
+
(window as any).lucide.createIcons();
|
|
228
315
|
}
|
|
229
316
|
});
|
|
230
317
|
|