zero-query 1.0.1 → 1.0.2
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 +47 -6
- package/cli/help.js +2 -1
- package/cli/scaffold/default/app/app.js +3 -6
- package/cli/scaffold/default/global.css +12 -3
- package/cli/scaffold/default/index.html +8 -8
- package/cli/scaffold/minimal/app/app.js +3 -9
- package/cli/scaffold/minimal/global.css +12 -3
- package/cli/scaffold/minimal/index.html +3 -3
- package/cli/scaffold/ssr/app/app.js +18 -6
- package/cli/scaffold/ssr/app/components/about.js +42 -15
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -0
- package/cli/scaffold/ssr/app/components/blog/post.js +78 -0
- package/cli/scaffold/ssr/app/routes.js +4 -2
- package/cli/scaffold/ssr/global.css +117 -1
- package/cli/scaffold/ssr/index.html +8 -2
- package/cli/scaffold/ssr/server/data/posts.js +144 -0
- package/cli/scaffold/ssr/server/index.js +151 -11
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +21 -6
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/component.js +17 -2
- package/tests/component.test.js +480 -0
- package/tests/test-minifier.js +4 -4
- package/types/misc.d.ts +23 -1
package/src/component.js
CHANGED
|
@@ -678,13 +678,28 @@ class Component {
|
|
|
678
678
|
if (el.contains(e.target)) continue;
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
-
// Key modifiers - filter keyboard events by key
|
|
681
|
+
// Key modifiers - filter keyboard events by key.
|
|
682
|
+
// Named shortcuts map common names to their e.key values.
|
|
683
|
+
// Any modifier not recognised as a built-in behaviour, timing,
|
|
684
|
+
// or system modifier is matched against e.key (case-insensitive)
|
|
685
|
+
// so that arbitrary keys work: .a, .f1, .+, .0, .arrowup, etc.
|
|
682
686
|
const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
|
|
687
|
+
const _nonKeyMods = new Set(['prevent','stop','self','once','outside','capture','passive','debounce','throttle','ctrl','shift','alt','meta']);
|
|
683
688
|
let keyFiltered = false;
|
|
684
|
-
for (
|
|
689
|
+
for (let mi = 0; mi < modifiers.length; mi++) {
|
|
690
|
+
const mod = modifiers[mi];
|
|
685
691
|
if (_keyMap[mod]) {
|
|
686
692
|
const keys = _keyMap[mod].split('|');
|
|
687
693
|
if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
|
|
694
|
+
} else if (_nonKeyMods.has(mod)) {
|
|
695
|
+
continue;
|
|
696
|
+
} else if (/^\d+$/.test(mod) && mi > 0 && (modifiers[mi - 1] === 'debounce' || modifiers[mi - 1] === 'throttle')) {
|
|
697
|
+
// Numeric value following debounce/throttle — skip (it's a ms parameter)
|
|
698
|
+
continue;
|
|
699
|
+
} else {
|
|
700
|
+
// Dynamic key match — compare modifier against e.key
|
|
701
|
+
// Case-insensitive: .a matches 'a' and 'A', .f1 matches 'F1'
|
|
702
|
+
if (!e.key || e.key.toLowerCase() !== mod.toLowerCase()) { keyFiltered = true; break; }
|
|
688
703
|
}
|
|
689
704
|
}
|
|
690
705
|
if (keyFiltered) continue;
|
package/tests/component.test.js
CHANGED
|
@@ -2742,6 +2742,486 @@ describe('component - combined key + system modifiers', () => {
|
|
|
2742
2742
|
});
|
|
2743
2743
|
|
|
2744
2744
|
|
|
2745
|
+
// ===========================================================================
|
|
2746
|
+
// Dynamic key modifiers - arbitrary keys matched against e.key
|
|
2747
|
+
// ===========================================================================
|
|
2748
|
+
|
|
2749
|
+
describe('component - dynamic key modifier: single letter keys', () => {
|
|
2750
|
+
it('.a fires only on "a" key (case-insensitive)', () => {
|
|
2751
|
+
let count = 0;
|
|
2752
|
+
component('dkey-a', {
|
|
2753
|
+
handler() { count++; },
|
|
2754
|
+
render() { return '<input @keydown.a="handler">'; },
|
|
2755
|
+
});
|
|
2756
|
+
document.body.innerHTML = '<dkey-a id="dka"></dkey-a>';
|
|
2757
|
+
mount('#dka', 'dkey-a');
|
|
2758
|
+
const input = document.querySelector('#dka input');
|
|
2759
|
+
|
|
2760
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
2761
|
+
expect(count).toBe(1);
|
|
2762
|
+
|
|
2763
|
+
// Uppercase A should also match (case-insensitive)
|
|
2764
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', bubbles: true }));
|
|
2765
|
+
expect(count).toBe(2);
|
|
2766
|
+
|
|
2767
|
+
// Other keys must NOT fire
|
|
2768
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
2769
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
2770
|
+
expect(count).toBe(2);
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
it('.z fires only on "z" key', () => {
|
|
2774
|
+
let count = 0;
|
|
2775
|
+
component('dkey-z', {
|
|
2776
|
+
handler() { count++; },
|
|
2777
|
+
render() { return '<input @keydown.z="handler">'; },
|
|
2778
|
+
});
|
|
2779
|
+
document.body.innerHTML = '<dkey-z id="dkz"></dkey-z>';
|
|
2780
|
+
mount('#dkz', 'dkey-z');
|
|
2781
|
+
const input = document.querySelector('#dkz input');
|
|
2782
|
+
|
|
2783
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', bubbles: true }));
|
|
2784
|
+
expect(count).toBe(1);
|
|
2785
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Z', bubbles: true }));
|
|
2786
|
+
expect(count).toBe(2);
|
|
2787
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
2788
|
+
expect(count).toBe(2);
|
|
2789
|
+
});
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
describe('component - dynamic key modifier: function keys', () => {
|
|
2793
|
+
it('.f1 fires only on F1', () => {
|
|
2794
|
+
let count = 0;
|
|
2795
|
+
component('dkey-f1', {
|
|
2796
|
+
handler() { count++; },
|
|
2797
|
+
render() { return '<div @keydown.f1="handler" tabindex="0">x</div>'; },
|
|
2798
|
+
});
|
|
2799
|
+
document.body.innerHTML = '<dkey-f1 id="dkf1"></dkey-f1>';
|
|
2800
|
+
mount('#dkf1', 'dkey-f1');
|
|
2801
|
+
const el = document.querySelector('#dkf1 div');
|
|
2802
|
+
|
|
2803
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F1', bubbles: true }));
|
|
2804
|
+
expect(count).toBe(1);
|
|
2805
|
+
|
|
2806
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true }));
|
|
2807
|
+
expect(count).toBe(1);
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
it('.f12 fires only on F12', () => {
|
|
2811
|
+
let count = 0;
|
|
2812
|
+
component('dkey-f12', {
|
|
2813
|
+
handler() { count++; },
|
|
2814
|
+
render() { return '<div @keydown.f12="handler" tabindex="0">x</div>'; },
|
|
2815
|
+
});
|
|
2816
|
+
document.body.innerHTML = '<dkey-f12 id="dkf12"></dkey-f12>';
|
|
2817
|
+
mount('#dkf12', 'dkey-f12');
|
|
2818
|
+
const el = document.querySelector('#dkf12 div');
|
|
2819
|
+
|
|
2820
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F12', bubbles: true }));
|
|
2821
|
+
expect(count).toBe(1);
|
|
2822
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F1', bubbles: true }));
|
|
2823
|
+
expect(count).toBe(1);
|
|
2824
|
+
});
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
describe('component - dynamic key modifier: numeric keys', () => {
|
|
2828
|
+
it('.0 fires only on "0" key (not confused with debounce/throttle ms)', () => {
|
|
2829
|
+
let count = 0;
|
|
2830
|
+
component('dkey-zero', {
|
|
2831
|
+
handler() { count++; },
|
|
2832
|
+
render() { return '<input @keydown.0="handler">'; },
|
|
2833
|
+
});
|
|
2834
|
+
document.body.innerHTML = '<dkey-zero id="dk0"></dkey-zero>';
|
|
2835
|
+
mount('#dk0', 'dkey-zero');
|
|
2836
|
+
const input = document.querySelector('#dk0 input');
|
|
2837
|
+
|
|
2838
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: '0', bubbles: true }));
|
|
2839
|
+
expect(count).toBe(1);
|
|
2840
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true }));
|
|
2841
|
+
expect(count).toBe(1);
|
|
2842
|
+
});
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
describe('component - dynamic key modifier: special/punctuation keys', () => {
|
|
2846
|
+
it('.+ fires on "+" key', () => {
|
|
2847
|
+
let count = 0;
|
|
2848
|
+
component('dkey-plus', {
|
|
2849
|
+
handler() { count++; },
|
|
2850
|
+
render() { return '<div @keydown.+="handler" tabindex="0">x</div>'; },
|
|
2851
|
+
});
|
|
2852
|
+
document.body.innerHTML = '<dkey-plus id="dkplus"></dkey-plus>';
|
|
2853
|
+
mount('#dkplus', 'dkey-plus');
|
|
2854
|
+
const el = document.querySelector('#dkplus div');
|
|
2855
|
+
|
|
2856
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '+', bubbles: true }));
|
|
2857
|
+
expect(count).toBe(1);
|
|
2858
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '-', bubbles: true }));
|
|
2859
|
+
expect(count).toBe(1);
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
it('.- fires on "-" key', () => {
|
|
2863
|
+
let count = 0;
|
|
2864
|
+
component('dkey-minus', {
|
|
2865
|
+
handler() { count++; },
|
|
2866
|
+
render() { return '<div @keydown.-="handler" tabindex="0">x</div>'; },
|
|
2867
|
+
});
|
|
2868
|
+
document.body.innerHTML = '<dkey-minus id="dkminus"></dkey-minus>';
|
|
2869
|
+
mount('#dkminus', 'dkey-minus');
|
|
2870
|
+
const el = document.querySelector('#dkminus div');
|
|
2871
|
+
|
|
2872
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '-', bubbles: true }));
|
|
2873
|
+
expect(count).toBe(1);
|
|
2874
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '+', bubbles: true }));
|
|
2875
|
+
expect(count).toBe(1);
|
|
2876
|
+
});
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
describe('component - dynamic key modifier combined with system modifiers', () => {
|
|
2880
|
+
it('.ctrl.s fires only on Ctrl+S', () => {
|
|
2881
|
+
let count = 0;
|
|
2882
|
+
component('dkey-ctrl-s', {
|
|
2883
|
+
handler() { count++; },
|
|
2884
|
+
render() { return '<div @keydown.ctrl.s="handler" tabindex="0">x</div>'; },
|
|
2885
|
+
});
|
|
2886
|
+
document.body.innerHTML = '<dkey-ctrl-s id="dkcs"></dkey-ctrl-s>';
|
|
2887
|
+
mount('#dkcs', 'dkey-ctrl-s');
|
|
2888
|
+
const el = document.querySelector('#dkcs div');
|
|
2889
|
+
|
|
2890
|
+
// S without Ctrl → no fire
|
|
2891
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true, ctrlKey: false }));
|
|
2892
|
+
expect(count).toBe(0);
|
|
2893
|
+
|
|
2894
|
+
// Ctrl without S → no fire
|
|
2895
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true, ctrlKey: true }));
|
|
2896
|
+
expect(count).toBe(0);
|
|
2897
|
+
|
|
2898
|
+
// Ctrl+S → fire
|
|
2899
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true, ctrlKey: true }));
|
|
2900
|
+
expect(count).toBe(1);
|
|
2901
|
+
|
|
2902
|
+
// Ctrl+Shift+S (uppercase) → also fires (case insensitive)
|
|
2903
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'S', bubbles: true, ctrlKey: true }));
|
|
2904
|
+
expect(count).toBe(2);
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
it('.meta.k fires only on Meta+K', () => {
|
|
2908
|
+
let count = 0;
|
|
2909
|
+
component('dkey-meta-k', {
|
|
2910
|
+
handler() { count++; },
|
|
2911
|
+
render() { return '<div @keydown.meta.k="handler" tabindex="0">x</div>'; },
|
|
2912
|
+
});
|
|
2913
|
+
document.body.innerHTML = '<dkey-meta-k id="dkmk"></dkey-meta-k>';
|
|
2914
|
+
mount('#dkmk', 'dkey-meta-k');
|
|
2915
|
+
const el = document.querySelector('#dkmk div');
|
|
2916
|
+
|
|
2917
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', bubbles: true, metaKey: false }));
|
|
2918
|
+
expect(count).toBe(0);
|
|
2919
|
+
|
|
2920
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', bubbles: true, metaKey: true }));
|
|
2921
|
+
expect(count).toBe(1);
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
it('.alt.shift.f fires only on Alt+Shift+F', () => {
|
|
2925
|
+
let count = 0;
|
|
2926
|
+
component('dkey-alt-sf', {
|
|
2927
|
+
handler() { count++; },
|
|
2928
|
+
render() { return '<div @keydown.alt.shift.f="handler" tabindex="0">x</div>'; },
|
|
2929
|
+
});
|
|
2930
|
+
document.body.innerHTML = '<dkey-alt-sf id="dkas"></dkey-alt-sf>';
|
|
2931
|
+
mount('#dkas', 'dkey-alt-sf');
|
|
2932
|
+
const el = document.querySelector('#dkas div');
|
|
2933
|
+
|
|
2934
|
+
// Missing shift
|
|
2935
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: true, shiftKey: false }));
|
|
2936
|
+
expect(count).toBe(0);
|
|
2937
|
+
|
|
2938
|
+
// Missing alt
|
|
2939
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: false, shiftKey: true }));
|
|
2940
|
+
expect(count).toBe(0);
|
|
2941
|
+
|
|
2942
|
+
// Both held
|
|
2943
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: true, shiftKey: true }));
|
|
2944
|
+
expect(count).toBe(1);
|
|
2945
|
+
});
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
describe('component - dynamic key modifier combined with behaviour modifiers', () => {
|
|
2949
|
+
it('.a.prevent calls preventDefault only on "a" key', () => {
|
|
2950
|
+
component('dkey-a-prev', {
|
|
2951
|
+
handler() {},
|
|
2952
|
+
render() { return '<input @keydown.a.prevent="handler">'; },
|
|
2953
|
+
});
|
|
2954
|
+
document.body.innerHTML = '<dkey-a-prev id="dkap"></dkey-a-prev>';
|
|
2955
|
+
mount('#dkap', 'dkey-a-prev');
|
|
2956
|
+
const input = document.querySelector('#dkap input');
|
|
2957
|
+
|
|
2958
|
+
const aEvt = new KeyboardEvent('keydown', { key: 'a', bubbles: true, cancelable: true });
|
|
2959
|
+
vi.spyOn(aEvt, 'preventDefault');
|
|
2960
|
+
input.dispatchEvent(aEvt);
|
|
2961
|
+
expect(aEvt.preventDefault).toHaveBeenCalled();
|
|
2962
|
+
|
|
2963
|
+
const bEvt = new KeyboardEvent('keydown', { key: 'b', bubbles: true, cancelable: true });
|
|
2964
|
+
vi.spyOn(bEvt, 'preventDefault');
|
|
2965
|
+
input.dispatchEvent(bEvt);
|
|
2966
|
+
expect(bEvt.preventDefault).not.toHaveBeenCalled();
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
it('.ctrl.s.prevent.stop calls both preventDefault and stopPropagation', () => {
|
|
2970
|
+
component('dkey-cs-ps', {
|
|
2971
|
+
handler() {},
|
|
2972
|
+
render() { return '<div @keydown.ctrl.s.prevent.stop="handler" tabindex="0">x</div>'; },
|
|
2973
|
+
});
|
|
2974
|
+
document.body.innerHTML = '<dkey-cs-ps id="dkcsps"></dkey-cs-ps>';
|
|
2975
|
+
mount('#dkcsps', 'dkey-cs-ps');
|
|
2976
|
+
const el = document.querySelector('#dkcsps div');
|
|
2977
|
+
|
|
2978
|
+
const evt = new KeyboardEvent('keydown', { key: 's', bubbles: true, cancelable: true, ctrlKey: true });
|
|
2979
|
+
vi.spyOn(evt, 'preventDefault');
|
|
2980
|
+
vi.spyOn(evt, 'stopPropagation');
|
|
2981
|
+
el.dispatchEvent(evt);
|
|
2982
|
+
expect(evt.preventDefault).toHaveBeenCalled();
|
|
2983
|
+
expect(evt.stopPropagation).toHaveBeenCalled();
|
|
2984
|
+
});
|
|
2985
|
+
});
|
|
2986
|
+
|
|
2987
|
+
describe('component - dynamic key modifier: named shortcuts still work', () => {
|
|
2988
|
+
it('named shortcuts take priority over dynamic matching', () => {
|
|
2989
|
+
// .space maps to ' ' (literal space char) via _keyMap, NOT "space" string
|
|
2990
|
+
let count = 0;
|
|
2991
|
+
component('dkey-space-prio', {
|
|
2992
|
+
handler() { count++; },
|
|
2993
|
+
render() { return '<button @keydown.space="handler">x</button>'; },
|
|
2994
|
+
});
|
|
2995
|
+
document.body.innerHTML = '<dkey-space-prio id="dksp"></dkey-space-prio>';
|
|
2996
|
+
mount('#dksp', 'dkey-space-prio');
|
|
2997
|
+
const btn = document.querySelector('#dksp button');
|
|
2998
|
+
|
|
2999
|
+
// ' ' is the actual e.key for Space
|
|
3000
|
+
btn.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
|
|
3001
|
+
expect(count).toBe(1);
|
|
3002
|
+
|
|
3003
|
+
// 'space' is NOT the e.key - should NOT fire because .space uses _keyMap
|
|
3004
|
+
btn.dispatchEvent(new KeyboardEvent('keydown', { key: 'space', bubbles: true }));
|
|
3005
|
+
expect(count).toBe(1);
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
it('.delete still matches both Delete and Backspace', () => {
|
|
3009
|
+
let count = 0;
|
|
3010
|
+
component('dkey-del-prio', {
|
|
3011
|
+
handler() { count++; },
|
|
3012
|
+
render() { return '<input @keydown.delete="handler">'; },
|
|
3013
|
+
});
|
|
3014
|
+
document.body.innerHTML = '<dkey-del-prio id="dkdp"></dkey-del-prio>';
|
|
3015
|
+
mount('#dkdp', 'dkey-del-prio');
|
|
3016
|
+
const input = document.querySelector('#dkdp input');
|
|
3017
|
+
|
|
3018
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
|
3019
|
+
expect(count).toBe(1);
|
|
3020
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true }));
|
|
3021
|
+
expect(count).toBe(2);
|
|
3022
|
+
});
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
describe('component - dynamic key modifier: no-key events are not filtered', () => {
|
|
3026
|
+
it('click events pass through dynamic key modifiers without e.key', () => {
|
|
3027
|
+
// A .prevent on a click should not be treated as a key filter
|
|
3028
|
+
let count = 0;
|
|
3029
|
+
component('dkey-click-ok', {
|
|
3030
|
+
handler() { count++; },
|
|
3031
|
+
render() { return '<button @click.prevent="handler">x</button>'; },
|
|
3032
|
+
});
|
|
3033
|
+
document.body.innerHTML = '<dkey-click-ok id="dkco"></dkey-click-ok>';
|
|
3034
|
+
mount('#dkco', 'dkey-click-ok');
|
|
3035
|
+
const btn = document.querySelector('#dkco button');
|
|
3036
|
+
|
|
3037
|
+
btn.click();
|
|
3038
|
+
expect(count).toBe(1);
|
|
3039
|
+
});
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
describe('component - dynamic key modifier: debounce/throttle ms values not treated as keys', () => {
|
|
3043
|
+
it('.debounce.300 does not treat "300" as a key filter', async () => {
|
|
3044
|
+
let count = 0;
|
|
3045
|
+
component('dkey-deb-num', {
|
|
3046
|
+
handler() { count++; },
|
|
3047
|
+
render() { return '<input @input.debounce.300="handler">'; },
|
|
3048
|
+
});
|
|
3049
|
+
document.body.innerHTML = '<dkey-deb-num id="dkdn"></dkey-deb-num>';
|
|
3050
|
+
mount('#dkdn', 'dkey-deb-num');
|
|
3051
|
+
const input = document.querySelector('#dkdn input');
|
|
3052
|
+
|
|
3053
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3054
|
+
// Should be debounced, not blocked by "300" key filter
|
|
3055
|
+
await new Promise(r => setTimeout(r, 350));
|
|
3056
|
+
expect(count).toBe(1);
|
|
3057
|
+
});
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
describe('component - dynamic key modifier: multiple dynamic keys on separate bindings', () => {
|
|
3061
|
+
it('separate @keydown.a and @keydown.b fire independently', () => {
|
|
3062
|
+
let aCount = 0, bCount = 0;
|
|
3063
|
+
component('dkey-ab', {
|
|
3064
|
+
handlerA() { aCount++; },
|
|
3065
|
+
handlerB() { bCount++; },
|
|
3066
|
+
render() { return '<input @keydown.a="handlerA" @keydown.b="handlerB">'; },
|
|
3067
|
+
});
|
|
3068
|
+
document.body.innerHTML = '<dkey-ab id="dkab"></dkey-ab>';
|
|
3069
|
+
mount('#dkab', 'dkey-ab');
|
|
3070
|
+
const input = document.querySelector('#dkab input');
|
|
3071
|
+
|
|
3072
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3073
|
+
expect(aCount).toBe(1);
|
|
3074
|
+
expect(bCount).toBe(0);
|
|
3075
|
+
|
|
3076
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
3077
|
+
expect(aCount).toBe(1);
|
|
3078
|
+
expect(bCount).toBe(1);
|
|
3079
|
+
|
|
3080
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'c', bubbles: true }));
|
|
3081
|
+
expect(aCount).toBe(1);
|
|
3082
|
+
expect(bCount).toBe(1);
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
describe('component - dynamic key modifier: keyup vs keydown', () => {
|
|
3087
|
+
it('.a works on keyup events too', () => {
|
|
3088
|
+
let downCount = 0, upCount = 0;
|
|
3089
|
+
component('dkey-updown', {
|
|
3090
|
+
onDown() { downCount++; },
|
|
3091
|
+
onUp() { upCount++; },
|
|
3092
|
+
render() { return '<input @keydown.a="onDown" @keyup.a="onUp">'; },
|
|
3093
|
+
});
|
|
3094
|
+
document.body.innerHTML = '<dkey-updown id="dkud"></dkey-updown>';
|
|
3095
|
+
mount('#dkud', 'dkey-updown');
|
|
3096
|
+
const input = document.querySelector('#dkud input');
|
|
3097
|
+
|
|
3098
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3099
|
+
expect(downCount).toBe(1);
|
|
3100
|
+
expect(upCount).toBe(0);
|
|
3101
|
+
|
|
3102
|
+
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'a', bubbles: true }));
|
|
3103
|
+
expect(downCount).toBe(1);
|
|
3104
|
+
expect(upCount).toBe(1);
|
|
3105
|
+
|
|
3106
|
+
// Wrong key on both → neither fires
|
|
3107
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
3108
|
+
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'b', bubbles: true }));
|
|
3109
|
+
expect(downCount).toBe(1);
|
|
3110
|
+
expect(upCount).toBe(1);
|
|
3111
|
+
});
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
describe('component - dynamic key modifier: edge cases with e.key', () => {
|
|
3115
|
+
it('event without e.key property does not fire dynamic key handler', () => {
|
|
3116
|
+
let count = 0;
|
|
3117
|
+
component('dkey-nokey', {
|
|
3118
|
+
handler() { count++; },
|
|
3119
|
+
render() { return '<div @keydown.a="handler" tabindex="0">x</div>'; },
|
|
3120
|
+
});
|
|
3121
|
+
document.body.innerHTML = '<dkey-nokey id="dknk"></dkey-nokey>';
|
|
3122
|
+
mount('#dknk', 'dkey-nokey');
|
|
3123
|
+
const el = document.querySelector('#dknk div');
|
|
3124
|
+
|
|
3125
|
+
// KeyboardEvent with no key specified → e.key is empty string
|
|
3126
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));
|
|
3127
|
+
expect(count).toBe(0);
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
it('case-insensitive match for multi-char keys like PageDown', () => {
|
|
3131
|
+
let count = 0;
|
|
3132
|
+
component('dkey-pgdn', {
|
|
3133
|
+
handler() { count++; },
|
|
3134
|
+
render() { return '<div @keydown.pagedown="handler" tabindex="0">x</div>'; },
|
|
3135
|
+
});
|
|
3136
|
+
document.body.innerHTML = '<dkey-pgdn id="dkpd"></dkey-pgdn>';
|
|
3137
|
+
mount('#dkpd', 'dkey-pgdn');
|
|
3138
|
+
const el = document.querySelector('#dkpd div');
|
|
3139
|
+
|
|
3140
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));
|
|
3141
|
+
expect(count).toBe(1);
|
|
3142
|
+
|
|
3143
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));
|
|
3144
|
+
expect(count).toBe(1);
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
it('.home and .end match Home / End keys', () => {
|
|
3148
|
+
let homeCount = 0, endCount = 0;
|
|
3149
|
+
component('dkey-homeend', {
|
|
3150
|
+
onHome() { homeCount++; },
|
|
3151
|
+
onEnd() { endCount++; },
|
|
3152
|
+
render() { return '<input @keydown.home="onHome" @keydown.end="onEnd">'; },
|
|
3153
|
+
});
|
|
3154
|
+
document.body.innerHTML = '<dkey-homeend id="dkhe"></dkey-homeend>';
|
|
3155
|
+
mount('#dkhe', 'dkey-homeend');
|
|
3156
|
+
const input = document.querySelector('#dkhe input');
|
|
3157
|
+
|
|
3158
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
|
|
3159
|
+
expect(homeCount).toBe(1);
|
|
3160
|
+
expect(endCount).toBe(0);
|
|
3161
|
+
|
|
3162
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
|
|
3163
|
+
expect(homeCount).toBe(1);
|
|
3164
|
+
expect(endCount).toBe(1);
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
it('.insert matches Insert key', () => {
|
|
3168
|
+
let count = 0;
|
|
3169
|
+
component('dkey-ins', {
|
|
3170
|
+
handler() { count++; },
|
|
3171
|
+
render() { return '<input @keydown.insert="handler">'; },
|
|
3172
|
+
});
|
|
3173
|
+
document.body.innerHTML = '<dkey-ins id="dkins"></dkey-ins>';
|
|
3174
|
+
mount('#dkins', 'dkey-ins');
|
|
3175
|
+
const input = document.querySelector('#dkins input');
|
|
3176
|
+
|
|
3177
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Insert', bubbles: true }));
|
|
3178
|
+
expect(count).toBe(1);
|
|
3179
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
|
3180
|
+
expect(count).toBe(1);
|
|
3181
|
+
});
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
describe('component - dynamic key modifier: non-interfering with existing modifiers', () => {
|
|
3185
|
+
it('.once still works with dynamic key', () => {
|
|
3186
|
+
let count = 0;
|
|
3187
|
+
component('dkey-once-a', {
|
|
3188
|
+
handler() { count++; },
|
|
3189
|
+
render() { return '<input @keydown.a.once="handler">'; },
|
|
3190
|
+
});
|
|
3191
|
+
document.body.innerHTML = '<dkey-once-a id="dkoa"></dkey-once-a>';
|
|
3192
|
+
mount('#dkoa', 'dkey-once-a');
|
|
3193
|
+
const input = document.querySelector('#dkoa input');
|
|
3194
|
+
|
|
3195
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3196
|
+
expect(count).toBe(1);
|
|
3197
|
+
|
|
3198
|
+
// Second press should NOT fire (.once)
|
|
3199
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3200
|
+
expect(count).toBe(1);
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
it('.self still works with dynamic key', () => {
|
|
3204
|
+
let count = 0;
|
|
3205
|
+
component('dkey-self-a', {
|
|
3206
|
+
handler() { count++; },
|
|
3207
|
+
render() { return '<div @keydown.a.self="handler" tabindex="0"><span>child</span></div>'; },
|
|
3208
|
+
});
|
|
3209
|
+
document.body.innerHTML = '<dkey-self-a id="dksa"></dkey-self-a>';
|
|
3210
|
+
mount('#dksa', 'dkey-self-a');
|
|
3211
|
+
const div = document.querySelector('#dksa div');
|
|
3212
|
+
const span = document.querySelector('#dksa span');
|
|
3213
|
+
|
|
3214
|
+
// Fire on div itself → should work
|
|
3215
|
+
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3216
|
+
expect(count).toBe(1);
|
|
3217
|
+
|
|
3218
|
+
// Fire from child → should NOT fire (.self)
|
|
3219
|
+
span.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3220
|
+
expect(count).toBe(1);
|
|
3221
|
+
});
|
|
3222
|
+
});
|
|
3223
|
+
|
|
3224
|
+
|
|
2745
3225
|
// ===========================================================================
|
|
2746
3226
|
// .outside modifier - fire when event target is outside the element
|
|
2747
3227
|
// ===========================================================================
|
package/tests/test-minifier.js
CHANGED
|
@@ -8,7 +8,7 @@ function minifyCSS(css) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const tests = [
|
|
11
|
-
// Pseudo-classes with descendant combinator (the bug
|
|
11
|
+
// Pseudo-classes with descendant combinator (the bug was that the space before :not() was being removed, which is incorrect)
|
|
12
12
|
['.docs-section :not(pre) > code { color: red }', 'space before :not()'],
|
|
13
13
|
['.foo :has(.bar) { color: red }', 'space before :has()'],
|
|
14
14
|
['.foo :is(.a, .b) { color: red }', 'space before :is()'],
|
|
@@ -18,12 +18,12 @@ const tests = [
|
|
|
18
18
|
['.foo::before { content: "x" }', '::before'],
|
|
19
19
|
['.foo ::after { content: "x" }', 'space before ::after'],
|
|
20
20
|
|
|
21
|
-
// Pseudo-classes directly on element (no space
|
|
21
|
+
// Pseudo-classes directly on element (no space - should stay compact)
|
|
22
22
|
['a:hover { color: red }', ':hover no space'],
|
|
23
23
|
['li:nth-child(2n + 1) { color: red }', ':nth-child()'],
|
|
24
24
|
['input:focus-visible { outline: 1px }', ':focus-visible'],
|
|
25
25
|
|
|
26
|
-
// calc()
|
|
26
|
+
// calc() - spaces around operators are significant!
|
|
27
27
|
['.foo { width: calc(100% - 20px) }', 'calc() spaces'],
|
|
28
28
|
['.foo { width: calc(50vw + 2rem) }', 'calc() addition'],
|
|
29
29
|
['.foo { font-size: clamp(1rem, 2vw, 3rem) }', 'clamp()'],
|
|
@@ -145,7 +145,7 @@ assert('.bar :not(.a):not(.b) { color: red }',
|
|
|
145
145
|
// Edge case: content with double spaces inside quotes
|
|
146
146
|
const dblSpace = minifyCSS('.foo::before { content: "a b" }');
|
|
147
147
|
if (dblSpace.includes('content:"a b"')) {
|
|
148
|
-
console.log('WARN content double space
|
|
148
|
+
console.log('WARN content double space - "a b" collapsed to "a b" (cosmetic, not functional)');
|
|
149
149
|
} else {
|
|
150
150
|
console.log('PASS content double space preserved');
|
|
151
151
|
}
|
package/types/misc.d.ts
CHANGED
|
@@ -165,15 +165,37 @@ export function safeEval(expr: string, scope: object[]): any;
|
|
|
165
165
|
/**
|
|
166
166
|
* Supported event modifier strings for `@event` and `z-on:event` bindings.
|
|
167
167
|
* Modifiers are appended to the event name with dots, e.g. `@click.prevent.stop`.
|
|
168
|
+
*
|
|
169
|
+
* **Key modifiers** — named shortcuts (`.enter`, `.escape`, `.tab`, `.space`,
|
|
170
|
+
* `.delete`, `.up`, `.down`, `.left`, `.right`) plus any arbitrary key matched
|
|
171
|
+
* case-insensitively against `KeyboardEvent.key` (e.g. `.a`, `.f1`, `.+`).
|
|
172
|
+
*
|
|
173
|
+
* **System modifiers** — `.ctrl`, `.shift`, `.alt`, `.meta` require the
|
|
174
|
+
* corresponding modifier key to be held.
|
|
168
175
|
*/
|
|
169
176
|
export type EventModifier =
|
|
170
177
|
| 'prevent'
|
|
171
178
|
| 'stop'
|
|
172
179
|
| 'self'
|
|
173
180
|
| 'once'
|
|
181
|
+
| 'outside'
|
|
174
182
|
| 'capture'
|
|
175
183
|
| 'passive'
|
|
176
184
|
| `debounce`
|
|
177
185
|
| `debounce.${number}`
|
|
178
186
|
| `throttle`
|
|
179
|
-
| `throttle.${number}
|
|
187
|
+
| `throttle.${number}`
|
|
188
|
+
| 'enter'
|
|
189
|
+
| 'escape'
|
|
190
|
+
| 'tab'
|
|
191
|
+
| 'space'
|
|
192
|
+
| 'delete'
|
|
193
|
+
| 'up'
|
|
194
|
+
| 'down'
|
|
195
|
+
| 'left'
|
|
196
|
+
| 'right'
|
|
197
|
+
| 'ctrl'
|
|
198
|
+
| 'shift'
|
|
199
|
+
| 'alt'
|
|
200
|
+
| 'meta'
|
|
201
|
+
| (string & {});
|