@vanduo-oss/framework 1.2.6 → 1.2.7
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 +31 -5
- package/css/components/affix.css +53 -0
- package/css/components/bubble.css +165 -0
- package/css/components/datepicker.css +216 -0
- package/css/components/fab.css +225 -0
- package/css/components/flow.css +265 -0
- package/css/components/rating.css +112 -0
- package/css/components/ripple.css +63 -0
- package/css/components/sidenav.css +70 -0
- package/css/components/spotlight.css +119 -0
- package/css/components/stepper.css +176 -0
- package/css/components/suggest.css +119 -0
- package/css/components/timeline.css +201 -0
- package/css/components/timepicker.css +80 -0
- package/css/components/transfer.css +165 -0
- package/css/components/tree.css +173 -0
- package/css/components/waypoint.css +59 -0
- package/css/vanduo.css +17 -0
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +2152 -4
- package/dist/vanduo.cjs.js.map +4 -4
- package/dist/vanduo.cjs.min.js +5 -5
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +1943 -1
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +2152 -4
- package/dist/vanduo.esm.js.map +4 -4
- package/dist/vanduo.esm.min.js +5 -5
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +2152 -4
- package/dist/vanduo.js.map +4 -4
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +5 -5
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/affix.js +129 -0
- package/js/components/bubble.js +203 -0
- package/js/components/datepicker.js +287 -0
- package/js/components/flow.js +264 -0
- package/js/components/rating.js +160 -0
- package/js/components/ripple.js +74 -0
- package/js/components/sidenav.js +9 -2
- package/js/components/spotlight.js +295 -0
- package/js/components/stepper.js +97 -0
- package/js/components/suggest.js +219 -0
- package/js/components/timepicker.js +142 -0
- package/js/components/transfer.js +206 -0
- package/js/components/tree.js +191 -0
- package/js/components/validate.js +185 -0
- package/js/components/waypoint.js +120 -0
- package/js/index.js +16 -0
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Timepicker Component
|
|
3
|
+
* Dropdown time selection with 12h/24h format and configurable step intervals
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Timepicker = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const inputs = document.querySelectorAll('[data-vd-timepicker]');
|
|
14
|
+
inputs.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (input) {
|
|
21
|
+
const cleanup = [];
|
|
22
|
+
const is24h = input.getAttribute('data-vd-timepicker-format') === '24h';
|
|
23
|
+
const step = parseInt(input.getAttribute('data-vd-timepicker-step') || '30', 10);
|
|
24
|
+
|
|
25
|
+
// Create wrapper
|
|
26
|
+
let wrapper = input.closest('.vd-suggest-wrapper');
|
|
27
|
+
if (!wrapper) {
|
|
28
|
+
wrapper = document.createElement('div');
|
|
29
|
+
wrapper.style.position = 'relative';
|
|
30
|
+
wrapper.style.display = 'inline-block';
|
|
31
|
+
input.parentNode.insertBefore(wrapper, input);
|
|
32
|
+
wrapper.appendChild(input);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create popup
|
|
36
|
+
const popup = document.createElement('div');
|
|
37
|
+
popup.className = 'vd-timepicker-popup';
|
|
38
|
+
popup.setAttribute('role', 'listbox');
|
|
39
|
+
wrapper.appendChild(popup);
|
|
40
|
+
|
|
41
|
+
// Generate time slots
|
|
42
|
+
const times = [];
|
|
43
|
+
for (let h = 0; h < 24; h++) {
|
|
44
|
+
for (let m = 0; m < 60; m += step) {
|
|
45
|
+
const hh24 = String(h).padStart(2, '0');
|
|
46
|
+
const mm = String(m).padStart(2, '0');
|
|
47
|
+
|
|
48
|
+
if (is24h) {
|
|
49
|
+
times.push({ display: hh24 + ':' + mm, value: hh24 + ':' + mm });
|
|
50
|
+
} else {
|
|
51
|
+
const period = h < 12 ? 'AM' : 'PM';
|
|
52
|
+
const h12 = h === 0 ? 12 : (h > 12 ? h - 12 : h);
|
|
53
|
+
const display = h12 + ':' + mm + ' ' + period;
|
|
54
|
+
times.push({ display, value: hh24 + ':' + mm });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const render = () => {
|
|
60
|
+
popup.innerHTML = '';
|
|
61
|
+
times.forEach(t => {
|
|
62
|
+
const item = document.createElement('div');
|
|
63
|
+
item.className = 'vd-timepicker-item';
|
|
64
|
+
item.setAttribute('role', 'option');
|
|
65
|
+
item.textContent = t.display;
|
|
66
|
+
|
|
67
|
+
if (input.value === t.value || input.value === t.display) {
|
|
68
|
+
item.classList.add('is-selected');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
item.addEventListener('click', () => {
|
|
72
|
+
input.value = t.display;
|
|
73
|
+
popup.querySelectorAll('.vd-timepicker-item').forEach(i => i.classList.remove('is-selected'));
|
|
74
|
+
item.classList.add('is-selected');
|
|
75
|
+
close();
|
|
76
|
+
input.dispatchEvent(new CustomEvent('timepicker:select', {
|
|
77
|
+
detail: { display: t.display, value: t.value },
|
|
78
|
+
bubbles: true
|
|
79
|
+
}));
|
|
80
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
popup.appendChild(item);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const open = () => {
|
|
88
|
+
render();
|
|
89
|
+
popup.classList.add('is-open');
|
|
90
|
+
input.setAttribute('aria-expanded', 'true');
|
|
91
|
+
// Scroll to selected
|
|
92
|
+
const selected = popup.querySelector('.is-selected');
|
|
93
|
+
if (selected) selected.scrollIntoView({ block: 'center' });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const close = () => {
|
|
97
|
+
popup.classList.remove('is-open');
|
|
98
|
+
input.setAttribute('aria-expanded', 'false');
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const focusHandler = () => open();
|
|
102
|
+
const outsideHandler = (e) => {
|
|
103
|
+
if (!wrapper.contains(e.target)) close();
|
|
104
|
+
};
|
|
105
|
+
const escHandler = (e) => { if (e.key === 'Escape') close(); };
|
|
106
|
+
|
|
107
|
+
input.addEventListener('focus', focusHandler);
|
|
108
|
+
document.addEventListener('click', outsideHandler, true);
|
|
109
|
+
document.addEventListener('keydown', escHandler);
|
|
110
|
+
input.setAttribute('aria-haspopup', 'listbox');
|
|
111
|
+
input.setAttribute('aria-expanded', 'false');
|
|
112
|
+
input.setAttribute('autocomplete', 'off');
|
|
113
|
+
input.readOnly = true;
|
|
114
|
+
|
|
115
|
+
cleanup.push(
|
|
116
|
+
() => input.removeEventListener('focus', focusHandler),
|
|
117
|
+
() => document.removeEventListener('click', outsideHandler, true),
|
|
118
|
+
() => document.removeEventListener('keydown', escHandler)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
this.instances.set(input, { cleanup, open, close });
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
destroy: function (el) {
|
|
125
|
+
const instance = this.instances.get(el);
|
|
126
|
+
if (!instance) return;
|
|
127
|
+
instance.cleanup.forEach(fn => fn());
|
|
128
|
+
this.instances.delete(el);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
destroyAll: function () {
|
|
132
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
137
|
+
window.Vanduo.register('timepicker', Timepicker);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
window.VanduoTimepicker = Timepicker;
|
|
141
|
+
|
|
142
|
+
})();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Transfer (Dual-list) Component
|
|
3
|
+
* Source-to-target list transfer with search, select-all, and move actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Transfer = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const transfers = document.querySelectorAll('[data-vd-transfer]');
|
|
14
|
+
transfers.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (el) {
|
|
21
|
+
const cleanup = [];
|
|
22
|
+
el.classList.add('vd-transfer');
|
|
23
|
+
|
|
24
|
+
let sourceData, targetData;
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(el.getAttribute('data-vd-transfer') || '[]');
|
|
27
|
+
sourceData = raw.map((item, i) => ({
|
|
28
|
+
id: item.id || 'item-' + i,
|
|
29
|
+
label: item.label || item.text || String(item),
|
|
30
|
+
selected: false
|
|
31
|
+
}));
|
|
32
|
+
} catch (_e) {
|
|
33
|
+
sourceData = [];
|
|
34
|
+
}
|
|
35
|
+
targetData = [];
|
|
36
|
+
|
|
37
|
+
const sourceSelected = new Set();
|
|
38
|
+
const targetSelected = new Set();
|
|
39
|
+
|
|
40
|
+
const render = () => {
|
|
41
|
+
el.innerHTML = '';
|
|
42
|
+
|
|
43
|
+
// Source panel
|
|
44
|
+
const sourcePanel = createPanel('Source', sourceData, sourceSelected, 'source');
|
|
45
|
+
// Actions
|
|
46
|
+
const actions = document.createElement('div');
|
|
47
|
+
actions.className = 'vd-transfer-actions';
|
|
48
|
+
|
|
49
|
+
const moveRightBtn = document.createElement('button');
|
|
50
|
+
moveRightBtn.type = 'button';
|
|
51
|
+
moveRightBtn.className = 'vd-transfer-btn';
|
|
52
|
+
moveRightBtn.innerHTML = '›';
|
|
53
|
+
moveRightBtn.setAttribute('aria-label', 'Move to target');
|
|
54
|
+
moveRightBtn.disabled = sourceSelected.size === 0;
|
|
55
|
+
moveRightBtn.addEventListener('click', () => moveRight());
|
|
56
|
+
|
|
57
|
+
const moveLeftBtn = document.createElement('button');
|
|
58
|
+
moveLeftBtn.type = 'button';
|
|
59
|
+
moveLeftBtn.className = 'vd-transfer-btn';
|
|
60
|
+
moveLeftBtn.innerHTML = '‹';
|
|
61
|
+
moveLeftBtn.setAttribute('aria-label', 'Move to source');
|
|
62
|
+
moveLeftBtn.disabled = targetSelected.size === 0;
|
|
63
|
+
moveLeftBtn.addEventListener('click', () => moveLeft());
|
|
64
|
+
|
|
65
|
+
actions.appendChild(moveRightBtn);
|
|
66
|
+
actions.appendChild(moveLeftBtn);
|
|
67
|
+
|
|
68
|
+
// Target panel
|
|
69
|
+
const targetPanel = createPanel('Target', targetData, targetSelected, 'target');
|
|
70
|
+
|
|
71
|
+
el.appendChild(sourcePanel);
|
|
72
|
+
el.appendChild(actions);
|
|
73
|
+
el.appendChild(targetPanel);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const createPanel = (title, data, selected, _side) => {
|
|
77
|
+
const panel = document.createElement('div');
|
|
78
|
+
panel.className = 'vd-transfer-panel';
|
|
79
|
+
|
|
80
|
+
const header = document.createElement('div');
|
|
81
|
+
header.className = 'vd-transfer-header';
|
|
82
|
+
const titleSpan = document.createElement('span');
|
|
83
|
+
titleSpan.textContent = title;
|
|
84
|
+
const count = document.createElement('span');
|
|
85
|
+
count.className = 'vd-transfer-count';
|
|
86
|
+
count.textContent = selected.size + '/' + data.length;
|
|
87
|
+
header.appendChild(titleSpan);
|
|
88
|
+
header.appendChild(count);
|
|
89
|
+
panel.appendChild(header);
|
|
90
|
+
|
|
91
|
+
// Search
|
|
92
|
+
const searchDiv = document.createElement('div');
|
|
93
|
+
searchDiv.className = 'vd-transfer-search';
|
|
94
|
+
const searchInput = document.createElement('input');
|
|
95
|
+
searchInput.type = 'text';
|
|
96
|
+
searchInput.placeholder = 'Search...';
|
|
97
|
+
searchInput.setAttribute('aria-label', 'Search ' + title.toLowerCase());
|
|
98
|
+
searchDiv.appendChild(searchInput);
|
|
99
|
+
panel.appendChild(searchDiv);
|
|
100
|
+
|
|
101
|
+
// List
|
|
102
|
+
const list = document.createElement('ul');
|
|
103
|
+
list.className = 'vd-transfer-list';
|
|
104
|
+
list.setAttribute('role', 'listbox');
|
|
105
|
+
|
|
106
|
+
const renderList = (filter) => {
|
|
107
|
+
list.innerHTML = '';
|
|
108
|
+
const filtered = filter ? data.filter(d => {
|
|
109
|
+
const label = (d.label || d.text || String(d)).toLowerCase();
|
|
110
|
+
return label.includes(filter.toLowerCase());
|
|
111
|
+
}) : data;
|
|
112
|
+
filtered.forEach(item => {
|
|
113
|
+
const li = document.createElement('li');
|
|
114
|
+
li.className = 'vd-transfer-item';
|
|
115
|
+
li.setAttribute('role', 'option');
|
|
116
|
+
if (selected.has(item.id)) li.classList.add('is-selected');
|
|
117
|
+
|
|
118
|
+
const checkbox = document.createElement('input');
|
|
119
|
+
checkbox.type = 'checkbox';
|
|
120
|
+
checkbox.checked = selected.has(item.id);
|
|
121
|
+
checkbox.setAttribute('aria-label', item.label);
|
|
122
|
+
|
|
123
|
+
const label = document.createElement('span');
|
|
124
|
+
label.textContent = item.label;
|
|
125
|
+
|
|
126
|
+
li.addEventListener('click', () => {
|
|
127
|
+
if (selected.has(item.id)) selected.delete(item.id);
|
|
128
|
+
else selected.add(item.id);
|
|
129
|
+
render();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
li.appendChild(checkbox);
|
|
133
|
+
li.appendChild(label);
|
|
134
|
+
list.appendChild(li);
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
searchInput.addEventListener('input', () => renderList(searchInput.value));
|
|
139
|
+
renderList('');
|
|
140
|
+
|
|
141
|
+
panel.appendChild(list);
|
|
142
|
+
return panel;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const moveRight = () => {
|
|
146
|
+
const toMove = sourceData.filter(d => sourceSelected.has(d.id));
|
|
147
|
+
sourceData = sourceData.filter(d => !sourceSelected.has(d.id));
|
|
148
|
+
targetData = targetData.concat(toMove);
|
|
149
|
+
sourceSelected.clear();
|
|
150
|
+
render();
|
|
151
|
+
fireChange();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const moveLeft = () => {
|
|
155
|
+
const toMove = targetData.filter(d => targetSelected.has(d.id));
|
|
156
|
+
targetData = targetData.filter(d => !targetSelected.has(d.id));
|
|
157
|
+
sourceData = sourceData.concat(toMove);
|
|
158
|
+
targetSelected.clear();
|
|
159
|
+
render();
|
|
160
|
+
fireChange();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const fireChange = () => {
|
|
164
|
+
el.dispatchEvent(new CustomEvent('transfer:change', {
|
|
165
|
+
detail: {
|
|
166
|
+
source: sourceData.map(d => d.id),
|
|
167
|
+
target: targetData.map(d => d.id)
|
|
168
|
+
},
|
|
169
|
+
bubbles: true
|
|
170
|
+
}));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
render();
|
|
174
|
+
|
|
175
|
+
this.instances.set(el, {
|
|
176
|
+
cleanup,
|
|
177
|
+
getTarget: () => targetData.map(d => d.id),
|
|
178
|
+
getSource: () => sourceData.map(d => d.id)
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
getSelected: function (el) {
|
|
183
|
+
const inst = this.instances.get(el);
|
|
184
|
+
return inst ? inst.getTarget() : [];
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
destroy: function (el) {
|
|
188
|
+
const inst = this.instances.get(el);
|
|
189
|
+
if (!inst) return;
|
|
190
|
+
inst.cleanup.forEach(fn => fn());
|
|
191
|
+
el.innerHTML = '';
|
|
192
|
+
this.instances.delete(el);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
destroyAll: function () {
|
|
196
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
201
|
+
window.Vanduo.register('transfer', Transfer);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
window.VanduoTransfer = Transfer;
|
|
205
|
+
|
|
206
|
+
})();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Tree View Component
|
|
3
|
+
* Hierarchical collapsible tree with checkbox selection and keyboard navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Tree = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const trees = document.querySelectorAll('[data-vd-tree]');
|
|
14
|
+
trees.forEach(el => {
|
|
15
|
+
if (this.instances.has(el)) return;
|
|
16
|
+
this.initInstance(el);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (el) {
|
|
21
|
+
const cleanup = [];
|
|
22
|
+
const cascade = el.getAttribute('data-vd-tree-cascade') !== 'false';
|
|
23
|
+
|
|
24
|
+
let data;
|
|
25
|
+
try { data = JSON.parse(el.getAttribute('data-vd-tree') || '[]'); } catch (_e) { data = []; }
|
|
26
|
+
|
|
27
|
+
el.classList.add('vd-tree');
|
|
28
|
+
el.setAttribute('role', 'tree');
|
|
29
|
+
|
|
30
|
+
const render = (items, parent) => {
|
|
31
|
+
parent.innerHTML = '';
|
|
32
|
+
items.forEach(item => {
|
|
33
|
+
const node = document.createElement('li');
|
|
34
|
+
node.className = 'vd-tree-node';
|
|
35
|
+
node.setAttribute('role', 'treeitem');
|
|
36
|
+
node.setAttribute('aria-expanded', item.open ? 'true' : 'false');
|
|
37
|
+
if (item.open) node.classList.add('is-open');
|
|
38
|
+
|
|
39
|
+
const content = document.createElement('div');
|
|
40
|
+
content.className = 'vd-tree-node-content';
|
|
41
|
+
|
|
42
|
+
// Toggle
|
|
43
|
+
if (item.children && item.children.length > 0) {
|
|
44
|
+
const toggle = document.createElement('button');
|
|
45
|
+
toggle.type = 'button';
|
|
46
|
+
toggle.className = 'vd-tree-toggle';
|
|
47
|
+
toggle.setAttribute('aria-label', 'Toggle');
|
|
48
|
+
toggle.addEventListener('click', (e) => {
|
|
49
|
+
e.stopPropagation();
|
|
50
|
+
item.open = !item.open;
|
|
51
|
+
node.classList.toggle('is-open');
|
|
52
|
+
node.setAttribute('aria-expanded', item.open ? 'true' : 'false');
|
|
53
|
+
});
|
|
54
|
+
content.appendChild(toggle);
|
|
55
|
+
} else {
|
|
56
|
+
const ph = document.createElement('span');
|
|
57
|
+
ph.className = 'vd-tree-toggle-placeholder';
|
|
58
|
+
content.appendChild(ph);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Checkbox
|
|
62
|
+
if (el.hasAttribute('data-vd-tree-checkbox')) {
|
|
63
|
+
const cb = document.createElement('input');
|
|
64
|
+
cb.type = 'checkbox';
|
|
65
|
+
cb.className = 'vd-tree-checkbox';
|
|
66
|
+
cb.checked = !!item.checked;
|
|
67
|
+
cb.setAttribute('aria-label', item.label);
|
|
68
|
+
cb.addEventListener('change', (e) => {
|
|
69
|
+
e.stopPropagation();
|
|
70
|
+
item.checked = cb.checked;
|
|
71
|
+
if (cascade && item.children) {
|
|
72
|
+
setChildChecked(item.children, cb.checked);
|
|
73
|
+
render(data, el);
|
|
74
|
+
}
|
|
75
|
+
el.dispatchEvent(new CustomEvent('tree:check', {
|
|
76
|
+
detail: { id: item.id, checked: cb.checked, label: item.label },
|
|
77
|
+
bubbles: true
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
content.appendChild(cb);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Icon
|
|
84
|
+
if (item.icon) {
|
|
85
|
+
const icon = document.createElement('span');
|
|
86
|
+
icon.className = 'vd-tree-icon ' + item.icon;
|
|
87
|
+
content.appendChild(icon);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Label
|
|
91
|
+
const label = document.createElement('span');
|
|
92
|
+
label.className = 'vd-tree-label';
|
|
93
|
+
label.textContent = item.label || '';
|
|
94
|
+
content.appendChild(label);
|
|
95
|
+
|
|
96
|
+
node.appendChild(content);
|
|
97
|
+
|
|
98
|
+
// Children
|
|
99
|
+
if (item.children && item.children.length > 0) {
|
|
100
|
+
const childList = document.createElement('ul');
|
|
101
|
+
childList.className = 'vd-tree-children';
|
|
102
|
+
childList.setAttribute('role', 'group');
|
|
103
|
+
render(item.children, childList);
|
|
104
|
+
node.appendChild(childList);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parent.appendChild(node);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const setChildChecked = (items, checked) => {
|
|
112
|
+
items.forEach(item => {
|
|
113
|
+
item.checked = checked;
|
|
114
|
+
if (item.children) setChildChecked(item.children, checked);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Keyboard
|
|
119
|
+
const keyHandler = (e) => {
|
|
120
|
+
const focused = document.activeElement;
|
|
121
|
+
if (!el.contains(focused)) return;
|
|
122
|
+
|
|
123
|
+
const nodes = Array.from(el.querySelectorAll('.vd-tree-node-content'));
|
|
124
|
+
const idx = nodes.indexOf(focused.closest('.vd-tree-node-content'));
|
|
125
|
+
if (idx === -1) return;
|
|
126
|
+
|
|
127
|
+
switch (e.key) {
|
|
128
|
+
case 'ArrowDown':
|
|
129
|
+
e.preventDefault();
|
|
130
|
+
if (idx < nodes.length - 1) {
|
|
131
|
+
const next = nodes[idx + 1].querySelector('.vd-tree-toggle, .vd-tree-label');
|
|
132
|
+
if (next) next.focus();
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'ArrowUp':
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
if (idx > 0) {
|
|
138
|
+
const prev = nodes[idx - 1].querySelector('.vd-tree-toggle, .vd-tree-label');
|
|
139
|
+
if (prev) prev.focus();
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
el.addEventListener('keydown', keyHandler);
|
|
146
|
+
cleanup.push(() => el.removeEventListener('keydown', keyHandler));
|
|
147
|
+
|
|
148
|
+
render(data, el);
|
|
149
|
+
|
|
150
|
+
this.instances.set(el, {
|
|
151
|
+
cleanup,
|
|
152
|
+
getData: () => data,
|
|
153
|
+
getChecked: () => {
|
|
154
|
+
const checked = [];
|
|
155
|
+
const collect = (items) => {
|
|
156
|
+
items.forEach(i => {
|
|
157
|
+
if (i.checked) checked.push(i.id || i.label);
|
|
158
|
+
if (i.children) collect(i.children);
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
collect(data);
|
|
162
|
+
return checked;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
getChecked: function (el) {
|
|
168
|
+
const inst = this.instances.get(el);
|
|
169
|
+
return inst ? inst.getChecked() : [];
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
destroy: function (el) {
|
|
173
|
+
const inst = this.instances.get(el);
|
|
174
|
+
if (!inst) return;
|
|
175
|
+
inst.cleanup.forEach(fn => fn());
|
|
176
|
+
el.innerHTML = '';
|
|
177
|
+
this.instances.delete(el);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
destroyAll: function () {
|
|
181
|
+
this.instances.forEach((_, el) => this.destroy(el));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
186
|
+
window.Vanduo.register('tree', Tree);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
window.VanduoTree = Tree;
|
|
190
|
+
|
|
191
|
+
})();
|