@vanduo-oss/framework 1.4.1 → 1.4.3
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 +3 -3
- package/css/components/datepicker.css +3 -1
- package/css/components/modals.css +10 -6
- package/css/components/timepicker.css +3 -1
- package/css/vanduo.css +0 -1
- package/dist/build-info.json +3 -3
- package/dist/vanduo.cjs.js +169 -1093
- package/dist/vanduo.cjs.js.map +3 -3
- package/dist/vanduo.cjs.min.js +8 -8
- package/dist/vanduo.cjs.min.js.map +4 -4
- package/dist/vanduo.css +17 -700
- package/dist/vanduo.css.map +1 -1
- package/dist/vanduo.esm.js +169 -1093
- package/dist/vanduo.esm.js.map +3 -3
- package/dist/vanduo.esm.min.js +8 -8
- package/dist/vanduo.esm.min.js.map +4 -4
- package/dist/vanduo.js +169 -1093
- package/dist/vanduo.js.map +3 -3
- package/dist/vanduo.min.css +2 -2
- package/dist/vanduo.min.css.map +1 -1
- package/dist/vanduo.min.js +8 -8
- package/dist/vanduo.min.js.map +4 -4
- package/js/components/code-snippet.js +73 -16
- package/js/components/datepicker.js +77 -4
- package/js/components/suggest.js +18 -15
- package/js/components/timepicker.js +48 -6
- package/js/index.js +0 -1
- package/package.json +1 -1
- package/css/components/music-player.css +0 -798
- package/js/components/music-player.js +0 -1294
|
@@ -566,19 +566,65 @@
|
|
|
566
566
|
* @returns {string} HTML with syntax highlighting spans
|
|
567
567
|
*/
|
|
568
568
|
highlightHtml: function (html) {
|
|
569
|
-
//
|
|
570
|
-
html = html.replace(/(<
|
|
569
|
+
// Comments first so tag highlighting does not treat them as elements.
|
|
570
|
+
html = html.replace(/(<!--)(.*?)(-->)/g, '<span class="code-comment">$1$2$3</span>');
|
|
571
571
|
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
html = html.replace(
|
|
572
|
+
// Only highlight inside escaped HTML tags. Build the highlighted tag from the
|
|
573
|
+
// original escaped source so regexes never re-process our injected spans.
|
|
574
|
+
html = html.replace(/<(?!--)([\s\S]*?)>/g, function (match, inner) {
|
|
575
|
+
const closingMatch = inner.match(/^(\/)([\w-]+)\s*$/);
|
|
576
|
+
if (closingMatch) {
|
|
577
|
+
return '<'
|
|
578
|
+
+ closingMatch[1]
|
|
579
|
+
+ '<span class="code-tag">' + closingMatch[2] + '</span>'
|
|
580
|
+
+ '>';
|
|
581
|
+
}
|
|
575
582
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
583
|
+
const selfClosingMatch = inner.match(/(\s*\/)\s*$/);
|
|
584
|
+
const selfClosingSuffix = selfClosingMatch ? selfClosingMatch[1] : '';
|
|
585
|
+
const tagSource = selfClosingMatch
|
|
586
|
+
? inner.slice(0, inner.length - selfClosingMatch[0].length)
|
|
587
|
+
: inner;
|
|
588
|
+
const openMatch = tagSource.match(/^([\w-]+)([\s\S]*)$/);
|
|
579
589
|
|
|
580
|
-
|
|
581
|
-
|
|
590
|
+
if (!openMatch) {
|
|
591
|
+
return match;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const tagName = openMatch[1];
|
|
595
|
+
let attrSource = openMatch[2];
|
|
596
|
+
|
|
597
|
+
attrSource = attrSource.replace(
|
|
598
|
+
/([\w:-]+)(\s*=\s*)(["'])([\s\S]*?)\3/g,
|
|
599
|
+
function (attrMatch, attrName, separator, quote, value) {
|
|
600
|
+
return '<span class="code-attr">' + attrName + '</span>'
|
|
601
|
+
+ separator
|
|
602
|
+
+ quote
|
|
603
|
+
+ '<span class="code-string">' + value + '</span>'
|
|
604
|
+
+ quote;
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
attrSource = attrSource.replace(
|
|
609
|
+
/([\w:-]+)(\s*=\s*)("|')([\s\S]*?)("|')/g,
|
|
610
|
+
function (attrMatch, attrName, separator, openQuote, value, closeQuote) {
|
|
611
|
+
if (openQuote !== closeQuote) {
|
|
612
|
+
return attrMatch;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return '<span class="code-attr">' + attrName + '</span>'
|
|
616
|
+
+ separator
|
|
617
|
+
+ openQuote
|
|
618
|
+
+ '<span class="code-string">' + value + '</span>'
|
|
619
|
+
+ closeQuote;
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return '<<span class="code-tag">' + tagName + '</span>'
|
|
624
|
+
+ attrSource
|
|
625
|
+
+ selfClosingSuffix
|
|
626
|
+
+ '>';
|
|
627
|
+
});
|
|
582
628
|
|
|
583
629
|
return html;
|
|
584
630
|
},
|
|
@@ -613,6 +659,20 @@
|
|
|
613
659
|
* @returns {string} JS with syntax highlighting spans
|
|
614
660
|
*/
|
|
615
661
|
highlightJs: function (js) {
|
|
662
|
+
const protectedTokens = [];
|
|
663
|
+
const protectToken = (value, className) => {
|
|
664
|
+
const marker = String.fromCharCode(0xE000 + protectedTokens.length);
|
|
665
|
+
protectedTokens.push({ marker, html: '<span class="' + className + '">' + value + '</span>' });
|
|
666
|
+
return marker;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Protect comments and strings before adding markup so later passes never
|
|
670
|
+
// re-highlight attributes inside the generated spans.
|
|
671
|
+
js = js.replace(
|
|
672
|
+
/\/\*[\s\S]*?\*\/|\/\/.*$|'(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`/gm,
|
|
673
|
+
match => protectToken(match, match.startsWith('/') ? 'code-comment' : 'code-string')
|
|
674
|
+
);
|
|
675
|
+
|
|
616
676
|
// Highlight keywords
|
|
617
677
|
const keywords = ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'new', 'this', 'class', 'extends', 'import', 'export', 'default', 'async', 'await', 'try', 'catch', 'throw', 'typeof', 'instanceof'];
|
|
618
678
|
keywords.forEach(kw => {
|
|
@@ -620,18 +680,15 @@
|
|
|
620
680
|
js = js.replace(regex, '<span class="code-keyword">$1</span>');
|
|
621
681
|
});
|
|
622
682
|
|
|
623
|
-
// Highlight strings (limit to 10 000 chars to prevent polynomial backtracking)
|
|
624
|
-
js = js.replace(/('(?:[^'\\]|\\.){0,10000}'|"(?:[^"\\]|\\.){0,10000}"|`(?:[^`\\]|\\.){0,10000}`)/g, '<span class="code-string">$1</span>');
|
|
625
|
-
|
|
626
683
|
// Highlight numbers
|
|
627
684
|
js = js.replace(/\b(\d+\.?\d*)\b/g, '<span class="code-number">$1</span>');
|
|
628
685
|
|
|
629
686
|
// Highlight function calls
|
|
630
687
|
js = js.replace(/\b([\w]+)(\s*\()/g, '<span class="code-function">$1</span>$2');
|
|
631
688
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
689
|
+
protectedTokens.forEach(token => {
|
|
690
|
+
js = js.replace(token.marker, token.html);
|
|
691
|
+
});
|
|
635
692
|
|
|
636
693
|
return js;
|
|
637
694
|
},
|
|
@@ -128,6 +128,32 @@
|
|
|
128
128
|
return x;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
132
|
+
const padding = 8;
|
|
133
|
+
const offset = gap != null ? gap : 4;
|
|
134
|
+
const rect = anchor.getBoundingClientRect();
|
|
135
|
+
|
|
136
|
+
popup.style.minWidth = Math.max(rect.width, 0) + 'px';
|
|
137
|
+
|
|
138
|
+
let top = rect.bottom + offset;
|
|
139
|
+
let left = rect.left;
|
|
140
|
+
popup.style.top = top + 'px';
|
|
141
|
+
popup.style.left = left + 'px';
|
|
142
|
+
|
|
143
|
+
const popRect = popup.getBoundingClientRect();
|
|
144
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
145
|
+
top = rect.top - popRect.height - offset;
|
|
146
|
+
popup.style.top = top + 'px';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
150
|
+
left = rect.left;
|
|
151
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
152
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
153
|
+
}
|
|
154
|
+
popup.style.left = Math.max(padding, left) + 'px';
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
const Datepicker = {
|
|
132
158
|
instances: new Map(),
|
|
133
159
|
|
|
@@ -155,6 +181,8 @@
|
|
|
155
181
|
let focusedDate = null;
|
|
156
182
|
/** Prevents focus() after close from immediately re-opening the popup */
|
|
157
183
|
let skipNextFocusOpen = false;
|
|
184
|
+
/** Ignore outside-click closes briefly after open (mobile click/focus ordering). */
|
|
185
|
+
let ignoreOutsideUntil = 0;
|
|
158
186
|
|
|
159
187
|
const isDisabled = (d) => {
|
|
160
188
|
if (minDate) {
|
|
@@ -221,7 +249,7 @@
|
|
|
221
249
|
wrapper.style.display = 'inline-block';
|
|
222
250
|
input.parentNode.insertBefore(wrapper, input);
|
|
223
251
|
wrapper.appendChild(input);
|
|
224
|
-
|
|
252
|
+
document.body.appendChild(popup);
|
|
225
253
|
|
|
226
254
|
const isSameDay = (a, b) => a && b &&
|
|
227
255
|
a.getFullYear() === b.getFullYear() &&
|
|
@@ -442,6 +470,10 @@
|
|
|
442
470
|
}
|
|
443
471
|
popup.appendChild(grid);
|
|
444
472
|
}
|
|
473
|
+
|
|
474
|
+
if (popup.classList.contains('is-open')) {
|
|
475
|
+
requestAnimationFrame(positionPopup);
|
|
476
|
+
}
|
|
445
477
|
};
|
|
446
478
|
|
|
447
479
|
const handleGridKeydown = (e) => {
|
|
@@ -523,7 +555,21 @@
|
|
|
523
555
|
requestAnimationFrame(focusFocusedDay);
|
|
524
556
|
};
|
|
525
557
|
|
|
558
|
+
const positionPopup = () => {
|
|
559
|
+
if (!popup.classList.contains('is-open')) return;
|
|
560
|
+
positionAnchoredPopup(input, popup);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const repositionHandler = () => {
|
|
564
|
+
positionPopup();
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const markIgnoreOutside = () => {
|
|
568
|
+
ignoreOutsideUntil = Date.now() + 100;
|
|
569
|
+
};
|
|
570
|
+
|
|
526
571
|
const open = () => {
|
|
572
|
+
markIgnoreOutside();
|
|
527
573
|
viewMode = 'days';
|
|
528
574
|
if (selectedDate) {
|
|
529
575
|
viewYear = selectedDate.getFullYear();
|
|
@@ -542,7 +588,10 @@
|
|
|
542
588
|
render();
|
|
543
589
|
popup.classList.add('is-open');
|
|
544
590
|
input.setAttribute('aria-expanded', 'true');
|
|
545
|
-
requestAnimationFrame(
|
|
591
|
+
requestAnimationFrame(() => {
|
|
592
|
+
positionPopup();
|
|
593
|
+
focusFocusedDay();
|
|
594
|
+
});
|
|
546
595
|
};
|
|
547
596
|
|
|
548
597
|
const close = () => {
|
|
@@ -558,8 +607,25 @@
|
|
|
558
607
|
}
|
|
559
608
|
open();
|
|
560
609
|
};
|
|
610
|
+
/** Mobile browsers may not focus the input before the click event (Playwright too). */
|
|
611
|
+
const clickHandler = () => {
|
|
612
|
+
if (!popup.classList.contains('is-open')) open();
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
const isOpenAnchorTarget = (target) => {
|
|
616
|
+
if (!target || !(target instanceof Node)) return false;
|
|
617
|
+
if (target === input || input.contains(target) || popup.contains(target)) return true;
|
|
618
|
+
const inputId = input.id;
|
|
619
|
+
if (inputId) {
|
|
620
|
+
const label = document.querySelector('label[for="' + inputId.replace(/"/g, '\\"') + '"]');
|
|
621
|
+
if (label && (target === label || label.contains(target))) return true;
|
|
622
|
+
}
|
|
623
|
+
return false;
|
|
624
|
+
};
|
|
625
|
+
|
|
561
626
|
const outsideHandler = (e) => {
|
|
562
|
-
if (
|
|
627
|
+
if (Date.now() < ignoreOutsideUntil) return;
|
|
628
|
+
if (!isOpenAnchorTarget(e.target)) close();
|
|
563
629
|
};
|
|
564
630
|
const escHandler = (e) => {
|
|
565
631
|
if (e.key === 'Escape' && popup.classList.contains('is-open')) {
|
|
@@ -570,9 +636,12 @@
|
|
|
570
636
|
};
|
|
571
637
|
|
|
572
638
|
input.addEventListener('focus', focusHandler);
|
|
639
|
+
input.addEventListener('click', clickHandler);
|
|
573
640
|
document.addEventListener('click', outsideHandler, true);
|
|
574
641
|
document.addEventListener('keydown', escHandler);
|
|
575
642
|
popup.addEventListener('keydown', handleGridKeydown);
|
|
643
|
+
window.addEventListener('resize', repositionHandler);
|
|
644
|
+
window.addEventListener('scroll', repositionHandler, true);
|
|
576
645
|
|
|
577
646
|
input.setAttribute('aria-haspopup', 'dialog');
|
|
578
647
|
input.setAttribute('aria-expanded', 'false');
|
|
@@ -580,9 +649,13 @@
|
|
|
580
649
|
|
|
581
650
|
cleanup.push(
|
|
582
651
|
() => input.removeEventListener('focus', focusHandler),
|
|
652
|
+
() => input.removeEventListener('click', clickHandler),
|
|
583
653
|
() => document.removeEventListener('click', outsideHandler, true),
|
|
584
654
|
() => document.removeEventListener('keydown', escHandler),
|
|
585
|
-
() => popup.removeEventListener('keydown', handleGridKeydown)
|
|
655
|
+
() => popup.removeEventListener('keydown', handleGridKeydown),
|
|
656
|
+
() => window.removeEventListener('resize', repositionHandler),
|
|
657
|
+
() => window.removeEventListener('scroll', repositionHandler, true),
|
|
658
|
+
() => popup.remove()
|
|
586
659
|
);
|
|
587
660
|
|
|
588
661
|
this.instances.set(input, { cleanup: cleanup, open: open, close: close, popup: popup });
|
package/js/components/suggest.js
CHANGED
|
@@ -6,17 +6,6 @@
|
|
|
6
6
|
(function () {
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Escape HTML entities to prevent XSS when inserting into innerHTML
|
|
11
|
-
* @param {string} text
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
function _escapeHtml(text) {
|
|
15
|
-
const div = document.createElement('div');
|
|
16
|
-
div.textContent = text;
|
|
17
|
-
return div.innerHTML;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
/**
|
|
21
10
|
* Allow same-origin URLs by default, or an explicit allowlist of origins.
|
|
22
11
|
* @param {string} url
|
|
@@ -109,10 +98,24 @@
|
|
|
109
98
|
|
|
110
99
|
const text = typeof item === 'object' ? (item.label || item.text || String(item)) : String(item);
|
|
111
100
|
if (query) {
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
const lowerText = text.toLowerCase();
|
|
102
|
+
const lowerQuery = query.toLowerCase();
|
|
103
|
+
let start = 0;
|
|
104
|
+
let matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
105
|
+
while (matchIndex !== -1) {
|
|
106
|
+
if (matchIndex > start) {
|
|
107
|
+
li.appendChild(document.createTextNode(text.slice(start, matchIndex)));
|
|
108
|
+
}
|
|
109
|
+
const matchSpan = document.createElement('span');
|
|
110
|
+
matchSpan.className = 'vd-suggest-match';
|
|
111
|
+
matchSpan.textContent = text.slice(matchIndex, matchIndex + query.length);
|
|
112
|
+
li.appendChild(matchSpan);
|
|
113
|
+
start = matchIndex + query.length;
|
|
114
|
+
matchIndex = lowerText.indexOf(lowerQuery, start);
|
|
115
|
+
}
|
|
116
|
+
if (start < text.length) {
|
|
117
|
+
li.appendChild(document.createTextNode(text.slice(start)));
|
|
118
|
+
}
|
|
116
119
|
} else {
|
|
117
120
|
li.textContent = text;
|
|
118
121
|
}
|
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
(function () {
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
function positionAnchoredPopup(anchor, popup, gap) {
|
|
10
|
+
const padding = 8;
|
|
11
|
+
const offset = gap != null ? gap : 4;
|
|
12
|
+
const rect = anchor.getBoundingClientRect();
|
|
13
|
+
|
|
14
|
+
popup.style.minWidth = Math.max(rect.width, 0) + 'px';
|
|
15
|
+
|
|
16
|
+
let top = rect.bottom + offset;
|
|
17
|
+
let left = rect.left;
|
|
18
|
+
popup.style.top = top + 'px';
|
|
19
|
+
popup.style.left = left + 'px';
|
|
20
|
+
|
|
21
|
+
const popRect = popup.getBoundingClientRect();
|
|
22
|
+
if (popRect.bottom > window.innerHeight - padding && rect.top - popRect.height > padding) {
|
|
23
|
+
top = rect.top - popRect.height - offset;
|
|
24
|
+
popup.style.top = top + 'px';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const alignedRect = popup.getBoundingClientRect();
|
|
28
|
+
left = rect.left;
|
|
29
|
+
if (left + alignedRect.width > window.innerWidth - padding) {
|
|
30
|
+
left = window.innerWidth - alignedRect.width - padding;
|
|
31
|
+
}
|
|
32
|
+
popup.style.left = Math.max(padding, left) + 'px';
|
|
33
|
+
}
|
|
34
|
+
|
|
9
35
|
const Timepicker = {
|
|
10
36
|
instances: new Map(),
|
|
11
37
|
|
|
@@ -36,7 +62,7 @@
|
|
|
36
62
|
const popup = document.createElement('div');
|
|
37
63
|
popup.className = 'vd-timepicker-popup';
|
|
38
64
|
popup.setAttribute('role', 'listbox');
|
|
39
|
-
|
|
65
|
+
document.body.appendChild(popup);
|
|
40
66
|
|
|
41
67
|
// Generate time slots
|
|
42
68
|
const times = [];
|
|
@@ -84,13 +110,24 @@
|
|
|
84
110
|
});
|
|
85
111
|
};
|
|
86
112
|
|
|
113
|
+
const positionPopup = () => {
|
|
114
|
+
if (!popup.classList.contains('is-open')) return;
|
|
115
|
+
positionAnchoredPopup(input, popup);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const repositionHandler = () => {
|
|
119
|
+
positionPopup();
|
|
120
|
+
};
|
|
121
|
+
|
|
87
122
|
const open = () => {
|
|
88
123
|
render();
|
|
89
124
|
popup.classList.add('is-open');
|
|
90
125
|
input.setAttribute('aria-expanded', 'true');
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
requestAnimationFrame(() => {
|
|
127
|
+
positionPopup();
|
|
128
|
+
const selected = popup.querySelector('.is-selected');
|
|
129
|
+
if (selected) selected.scrollIntoView({ block: 'center' });
|
|
130
|
+
});
|
|
94
131
|
};
|
|
95
132
|
|
|
96
133
|
const close = () => {
|
|
@@ -100,13 +137,15 @@
|
|
|
100
137
|
|
|
101
138
|
const focusHandler = () => open();
|
|
102
139
|
const outsideHandler = (e) => {
|
|
103
|
-
if (!
|
|
140
|
+
if (!input.contains(e.target) && !popup.contains(e.target)) close();
|
|
104
141
|
};
|
|
105
142
|
const escHandler = (e) => { if (e.key === 'Escape') close(); };
|
|
106
143
|
|
|
107
144
|
input.addEventListener('focus', focusHandler);
|
|
108
145
|
document.addEventListener('click', outsideHandler, true);
|
|
109
146
|
document.addEventListener('keydown', escHandler);
|
|
147
|
+
window.addEventListener('resize', repositionHandler);
|
|
148
|
+
window.addEventListener('scroll', repositionHandler, true);
|
|
110
149
|
input.setAttribute('aria-haspopup', 'listbox');
|
|
111
150
|
input.setAttribute('aria-expanded', 'false');
|
|
112
151
|
input.setAttribute('autocomplete', 'off');
|
|
@@ -115,7 +154,10 @@
|
|
|
115
154
|
cleanup.push(
|
|
116
155
|
() => input.removeEventListener('focus', focusHandler),
|
|
117
156
|
() => document.removeEventListener('click', outsideHandler, true),
|
|
118
|
-
() => document.removeEventListener('keydown', escHandler)
|
|
157
|
+
() => document.removeEventListener('keydown', escHandler),
|
|
158
|
+
() => window.removeEventListener('resize', repositionHandler),
|
|
159
|
+
() => window.removeEventListener('scroll', repositionHandler, true),
|
|
160
|
+
() => popup.remove()
|
|
119
161
|
);
|
|
120
162
|
|
|
121
163
|
this.instances.set(input, { cleanup, open, close });
|
package/js/index.js
CHANGED