@vanduo-oss/framework 1.2.6 → 1.2.8
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 +39 -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 +2161 -6
- 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 +1947 -5
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +2161 -6
- 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 +2161 -6
- 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/theme-customizer.js +11 -2
- package/js/components/theme-switcher.js +7 -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 +4 -4
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Validate (Form Validation) Component
|
|
3
|
+
* Declarative validation via data attributes with real-time and on-submit modes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Validate = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
rules: {
|
|
13
|
+
required: (value) => value.trim().length > 0,
|
|
14
|
+
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
15
|
+
url: (value) => { try { new URL(value); return true; } catch (_e) { return false; } },
|
|
16
|
+
number: (value) => !isNaN(parseFloat(value)) && isFinite(value),
|
|
17
|
+
min: (value, param) => value.length >= parseInt(param, 10),
|
|
18
|
+
max: (value, param) => value.length <= parseInt(param, 10),
|
|
19
|
+
minVal: (value, param) => parseFloat(value) >= parseFloat(param),
|
|
20
|
+
maxVal: (value, param) => parseFloat(value) <= parseFloat(param),
|
|
21
|
+
pattern: (value, param) => { try { return new RegExp(param).test(value); } catch (_e) { return false; } },
|
|
22
|
+
match: (value, param) => {
|
|
23
|
+
const other = document.querySelector('[name="' + param + '"]');
|
|
24
|
+
return other ? value === other.value : false;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
messages: {
|
|
29
|
+
required: 'This field is required',
|
|
30
|
+
email: 'Please enter a valid email address',
|
|
31
|
+
url: 'Please enter a valid URL',
|
|
32
|
+
number: 'Please enter a valid number',
|
|
33
|
+
min: 'Minimum {0} characters required',
|
|
34
|
+
max: 'Maximum {0} characters allowed',
|
|
35
|
+
minVal: 'Value must be at least {0}',
|
|
36
|
+
maxVal: 'Value must be at most {0}',
|
|
37
|
+
pattern: 'Invalid format',
|
|
38
|
+
match: 'Fields do not match'
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
init: function () {
|
|
42
|
+
const forms = document.querySelectorAll('[data-vd-validate], .vd-validate');
|
|
43
|
+
forms.forEach(form => {
|
|
44
|
+
if (this.instances.has(form)) return;
|
|
45
|
+
this.initInstance(form);
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
initInstance: function (form) {
|
|
50
|
+
const cleanup = [];
|
|
51
|
+
const mode = form.getAttribute('data-vd-validate-mode') || 'blur'; // blur | input | submit
|
|
52
|
+
const fields = form.querySelectorAll('[data-vd-rules]');
|
|
53
|
+
|
|
54
|
+
const validateField = (field) => {
|
|
55
|
+
const rulesStr = field.getAttribute('data-vd-rules') || '';
|
|
56
|
+
const rules = rulesStr.split('|').map(r => r.trim()).filter(Boolean);
|
|
57
|
+
const value = field.value;
|
|
58
|
+
const errors = [];
|
|
59
|
+
|
|
60
|
+
for (const rule of rules) {
|
|
61
|
+
const [name, ...params] = rule.split(':');
|
|
62
|
+
const param = params.join(':');
|
|
63
|
+
const validator = this.rules[name];
|
|
64
|
+
|
|
65
|
+
if (validator && !validator(value, param)) {
|
|
66
|
+
const customMsg = field.getAttribute('data-vd-msg-' + name);
|
|
67
|
+
let msg = customMsg || this.messages[name] || 'Invalid';
|
|
68
|
+
if (param) msg = msg.replace('{0}', param);
|
|
69
|
+
errors.push(msg);
|
|
70
|
+
break; // one error at a time
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.setFieldState(field, errors);
|
|
75
|
+
return errors.length === 0;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const validateAll = () => {
|
|
79
|
+
let valid = true;
|
|
80
|
+
fields.forEach(field => {
|
|
81
|
+
if (!validateField(field)) valid = false;
|
|
82
|
+
});
|
|
83
|
+
return valid;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Per-field listeners
|
|
87
|
+
fields.forEach(field => {
|
|
88
|
+
if (mode === 'input' || mode === 'blur') {
|
|
89
|
+
const eventType = mode === 'input' ? 'input' : 'blur';
|
|
90
|
+
const handler = () => validateField(field);
|
|
91
|
+
field.addEventListener(eventType, handler);
|
|
92
|
+
cleanup.push(() => field.removeEventListener(eventType, handler));
|
|
93
|
+
|
|
94
|
+
if (mode === 'blur') {
|
|
95
|
+
const inputClear = () => {
|
|
96
|
+
if (field.classList.contains('is-invalid') || field.classList.contains('is-valid')) {
|
|
97
|
+
validateField(field);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
field.addEventListener('input', inputClear);
|
|
101
|
+
cleanup.push(() => field.removeEventListener('input', inputClear));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Form submit
|
|
107
|
+
const submitHandler = (e) => {
|
|
108
|
+
const valid = validateAll();
|
|
109
|
+
if (!valid) {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
e.stopPropagation();
|
|
112
|
+
// Focus first invalid field
|
|
113
|
+
const firstInvalid = form.querySelector('.is-invalid');
|
|
114
|
+
if (firstInvalid) firstInvalid.focus();
|
|
115
|
+
}
|
|
116
|
+
form.dispatchEvent(new CustomEvent('validate:submit', {
|
|
117
|
+
detail: { valid },
|
|
118
|
+
bubbles: true
|
|
119
|
+
}));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
form.addEventListener('submit', submitHandler);
|
|
123
|
+
cleanup.push(() => form.removeEventListener('submit', submitHandler));
|
|
124
|
+
|
|
125
|
+
this.instances.set(form, { cleanup, validateAll, validateField });
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
setFieldState: function (field, errors) {
|
|
129
|
+
const wrapper = field.closest('.vd-form-group') || field.parentElement;
|
|
130
|
+
let errorEl = wrapper.querySelector('.vd-validate-error');
|
|
131
|
+
|
|
132
|
+
field.classList.remove('is-valid', 'is-invalid');
|
|
133
|
+
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
field.classList.add('is-invalid');
|
|
136
|
+
field.setAttribute('aria-invalid', 'true');
|
|
137
|
+
if (!errorEl) {
|
|
138
|
+
errorEl = document.createElement('div');
|
|
139
|
+
errorEl.className = 'vd-validate-error';
|
|
140
|
+
errorEl.id = 'vd-err-' + Math.random().toString(36).slice(2, 9);
|
|
141
|
+
errorEl.setAttribute('role', 'alert');
|
|
142
|
+
wrapper.appendChild(errorEl);
|
|
143
|
+
}
|
|
144
|
+
errorEl.textContent = errors[0];
|
|
145
|
+
errorEl.style.display = '';
|
|
146
|
+
field.setAttribute('aria-describedby', errorEl.id);
|
|
147
|
+
} else if (field.value.trim()) {
|
|
148
|
+
field.classList.add('is-valid');
|
|
149
|
+
field.removeAttribute('aria-invalid');
|
|
150
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
151
|
+
} else {
|
|
152
|
+
field.removeAttribute('aria-invalid');
|
|
153
|
+
if (errorEl) errorEl.style.display = 'none';
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
validateForm: function (form) {
|
|
158
|
+
const instance = this.instances.get(form);
|
|
159
|
+
return instance ? instance.validateAll() : false;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
addRule: function (name, validator, message) {
|
|
163
|
+
this.rules[name] = validator;
|
|
164
|
+
if (message) this.messages[name] = message;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
destroy: function (form) {
|
|
168
|
+
const instance = this.instances.get(form);
|
|
169
|
+
if (!instance) return;
|
|
170
|
+
instance.cleanup.forEach(fn => fn());
|
|
171
|
+
this.instances.delete(form);
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
destroyAll: function () {
|
|
175
|
+
this.instances.forEach((_, form) => this.destroy(form));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
180
|
+
window.Vanduo.register('validate', Validate);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
window.VanduoValidate = Validate;
|
|
184
|
+
|
|
185
|
+
})();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vanduo Framework - Waypoint (Scrollspy) Component
|
|
3
|
+
* Highlights navigation links based on scroll position using IntersectionObserver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
(function () {
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const Waypoint = {
|
|
10
|
+
instances: new Map(),
|
|
11
|
+
|
|
12
|
+
init: function () {
|
|
13
|
+
const navs = document.querySelectorAll('[data-vd-waypoint-nav], [data-vd-scrollspy-nav]');
|
|
14
|
+
navs.forEach(nav => {
|
|
15
|
+
if (this.instances.has(nav)) return;
|
|
16
|
+
this.initInstance(nav);
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
initInstance: function (nav) {
|
|
21
|
+
const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
|
|
22
|
+
if (links.length === 0) return;
|
|
23
|
+
|
|
24
|
+
const cleanup = [];
|
|
25
|
+
const offset = parseInt(nav.getAttribute('data-vd-waypoint-offset') || '80', 10);
|
|
26
|
+
const sections = [];
|
|
27
|
+
|
|
28
|
+
links.forEach(link => {
|
|
29
|
+
const id = link.getAttribute('href').slice(1);
|
|
30
|
+
const section = document.getElementById(id);
|
|
31
|
+
if (section) {
|
|
32
|
+
section.setAttribute('data-vd-waypoint-section', '');
|
|
33
|
+
sections.push({ id, link, section });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (sections.length === 0) return;
|
|
38
|
+
|
|
39
|
+
const activeSections = new Set();
|
|
40
|
+
|
|
41
|
+
const setActive = (id) => {
|
|
42
|
+
links.forEach(l => l.classList.remove('is-active'));
|
|
43
|
+
const target = links.find(l => l.getAttribute('href') === '#' + id);
|
|
44
|
+
if (target) {
|
|
45
|
+
target.classList.add('is-active');
|
|
46
|
+
nav.dispatchEvent(new CustomEvent('waypoint:change', {
|
|
47
|
+
detail: { activeId: id, link: target }
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const rootMargin = '-' + offset + 'px 0px -40% 0px';
|
|
53
|
+
|
|
54
|
+
const observer = new IntersectionObserver((entries) => {
|
|
55
|
+
entries.forEach(entry => {
|
|
56
|
+
if (entry.isIntersecting) {
|
|
57
|
+
activeSections.add(entry.target.id);
|
|
58
|
+
} else {
|
|
59
|
+
activeSections.delete(entry.target.id);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Pick the topmost visible section
|
|
64
|
+
for (let i = 0; i < sections.length; i++) {
|
|
65
|
+
if (activeSections.has(sections[i].id)) {
|
|
66
|
+
setActive(sections[i].id);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, {
|
|
71
|
+
rootMargin: rootMargin,
|
|
72
|
+
threshold: 0
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
sections.forEach(s => observer.observe(s.section));
|
|
76
|
+
|
|
77
|
+
// Smooth scroll on click
|
|
78
|
+
links.forEach(link => {
|
|
79
|
+
const clickHandler = (e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
const id = link.getAttribute('href').slice(1);
|
|
82
|
+
const section = document.getElementById(id);
|
|
83
|
+
if (section) {
|
|
84
|
+
section.scrollIntoView({ behavior: 'smooth' });
|
|
85
|
+
setActive(id);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
link.addEventListener('click', clickHandler);
|
|
89
|
+
cleanup.push(() => link.removeEventListener('click', clickHandler));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
cleanup.push(() => observer.disconnect());
|
|
93
|
+
|
|
94
|
+
this.instances.set(nav, { observer, cleanup, sections, setActive });
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
refresh: function (nav) {
|
|
98
|
+
this.destroy(nav);
|
|
99
|
+
this.initInstance(nav);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
destroy: function (nav) {
|
|
103
|
+
const instance = this.instances.get(nav);
|
|
104
|
+
if (!instance) return;
|
|
105
|
+
instance.cleanup.forEach(fn => fn());
|
|
106
|
+
this.instances.delete(nav);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
destroyAll: function () {
|
|
110
|
+
this.instances.forEach((_, nav) => this.destroy(nav));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (typeof window.Vanduo !== 'undefined') {
|
|
115
|
+
window.Vanduo.register('waypoint', Waypoint);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
window.VanduoWaypoint = Waypoint;
|
|
119
|
+
|
|
120
|
+
})();
|
package/js/index.js
CHANGED
|
@@ -47,6 +47,22 @@ import './components/doc-search.js';
|
|
|
47
47
|
import './components/draggable.js';
|
|
48
48
|
import './components/lazy-load.js';
|
|
49
49
|
|
|
50
|
+
// Phase 10 (v1.2.7) components
|
|
51
|
+
import './components/flow.js';
|
|
52
|
+
import './components/bubble.js';
|
|
53
|
+
import './components/waypoint.js';
|
|
54
|
+
import './components/ripple.js';
|
|
55
|
+
import './components/affix.js';
|
|
56
|
+
import './components/suggest.js';
|
|
57
|
+
import './components/validate.js';
|
|
58
|
+
import './components/datepicker.js';
|
|
59
|
+
import './components/timepicker.js';
|
|
60
|
+
import './components/stepper.js';
|
|
61
|
+
import './components/rating.js';
|
|
62
|
+
import './components/transfer.js';
|
|
63
|
+
import './components/tree.js';
|
|
64
|
+
import './components/spotlight.js';
|
|
65
|
+
|
|
50
66
|
// Re-export for ESM / CJS consumers
|
|
51
67
|
const Vanduo = window.Vanduo;
|
|
52
68
|
export { Vanduo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanduo-oss/framework",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "Zero-dependency CSS/JS framework built on Fibonacci/Golden Ratio design system with Open Color integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -41,10 +41,10 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@eslint/js": "^10.0.1",
|
|
43
43
|
"@playwright/test": "^1.58.2",
|
|
44
|
-
"esbuild": "^0.27.
|
|
45
|
-
"eslint": "^10.0.
|
|
44
|
+
"esbuild": "^0.27.4",
|
|
45
|
+
"eslint": "^10.0.3",
|
|
46
46
|
"husky": "^9.1.7",
|
|
47
|
-
"lightningcss": "^1.
|
|
47
|
+
"lightningcss": "^1.32.0",
|
|
48
48
|
"stylelint": "^17.4.0",
|
|
49
49
|
"stylelint-config-standard": "^40.0.0"
|
|
50
50
|
},
|