diff --git a/dist/cdn.js b/dist/cdn.js index ad02013..ae236d9 100644 --- a/dist/cdn.js +++ b/dist/cdn.js @@ -5,8 +5,10 @@ init() { let disabled = Alpine2.extractProp(this.$el, "disabled", false, false); let value = Alpine2.extractProp(this.$el, "value", ""); + const rawSearch = Alpine2.extractProp(this.$el, "data-search", value); + const normalizedSearch = String(rawSearch).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); this.$el.dataset.value = value; - this.__add(value, disabled); + this.__add(value, normalizedSearch, disabled); this.$nextTick(() => { if (disabled) { this.$el.setAttribute("tabindex", "-1"); @@ -33,8 +35,8 @@ this.activeIndex = Alpine.reactive({value: void 0}); this.searchThreshold = options.searchThreshold ?? 500; } - add(value, disabled = false) { - const item = {value, disabled}; + add(value, searchable, disabled = false) { + const item = {value, disabled, searchable}; this.items.push(item); this.invalidate(); } @@ -87,22 +89,20 @@ this.rebuildNavIndex(); return this.items; } - const normalized = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - if (this.currentQuery && normalized.startsWith(this.currentQuery) && this.currentResults.length > 0) { + const normalizedQuery = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + if (this.currentQuery && normalizedQuery.startsWith(this.currentQuery) && this.currentResults.length > 0) { const filtered = this.currentResults.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = filtered; this.rebuildNavIndex(); return filtered; } const results = this.items.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = results; this.rebuildNavIndex(); return results; @@ -209,14 +209,18 @@ // src/Managers/InputManager.ts function createInputManager(rootDataStack) { - const inputEl = rootDataStack.$el.querySelector("[x-rover\\:input]"); - if (!inputEl) { - console.warn(`Input element with [x-rover\\:input] not found`); - } + const inputEl = rootDataStack.$root.querySelector("[x-rover\\:input]"); + const inputElExists = () => { + if (!inputEl) { + console.warn(`Input element with [x-rover\\:input] not found`); + return false; + } + return true; + }; return { controller: new AbortController(), on(eventKey, handler) { - if (!inputEl) + if (!inputElExists()) return; const listener = (event) => { handler(event, rootDataStack.__activatedValue ?? void 0); @@ -230,8 +234,11 @@ if (inputEl) inputEl.value = val; }, + focus(preventScroll = true) { + requestAnimationFrame(() => inputEl?.focus({preventScroll})); + }, enableDefaultInputHandlers(disabledEvents = []) { - if (!inputEl) + if (!inputElExists()) return; if (!disabledEvents.includes("focus")) { this.on("focus", () => rootDataStack.__startTyping()); @@ -255,7 +262,18 @@ case "Escape": e.preventDefault(); e.stopPropagation(); - requestAnimationFrame(() => inputEl?.focus({preventScroll: true})); + requestAnimationFrame(() => this.focus(true)); + break; + case "Home": + e.preventDefault(); + rootDataStack.__activateFirst(); + break; + case "End": + e.preventDefault(); + rootDataStack.__activateLast(); + break; + case "Tab": + rootDataStack.__stopTyping(); break; } }); @@ -314,38 +332,62 @@ }, findClosestOption, enableDefaultOptionsHandlers(disabledEvents = []) { - const events = { - click: (optionEl) => { - if (!optionEl.dataset.value) - return; - root.$nextTick(() => root.$refs.__input?.focus({preventScroll: true})); - }, - mouseover: (optionEl) => { - if (!optionEl.dataset.value) + if (!optionsEl) + return; + optionsEl.tabIndex = 0; + if (!disabledEvents.includes("mouseover")) { + this.on("mouseover", (_event, optionEl) => { + if (!optionEl?.dataset.value) return; root.__activate(optionEl.dataset.value); - }, - mousemove: (optionEl) => { - if (!optionEl.dataset.value || root.__isActive(optionEl.dataset.value)) + }); + } + if (!disabledEvents.includes("mousemove")) { + this.on("mousemove", (_event, optionEl) => { + if (!optionEl?.dataset.value) + return; + if (root.__isActive(optionEl.dataset.value)) return; root.__activate(optionEl.dataset.value); - }, - mouseout: () => { + }); + } + if (!disabledEvents.includes("mouseout")) { + this.on("mouseout", () => { if (root.__keepActivated) return; root.__deactivate(); - } - }; - Object.entries(events).forEach(([key, handler]) => { - if (!disabledEvents.includes(key)) { - this.on(key, (event, optionEl) => { - event.stopPropagation(); - if (!optionEl) - return; - handler(optionEl); - }); - } - }); + }); + } + if (!disabledEvents.includes("keydown")) { + this.on("keydown", (event) => { + event.stopPropagation(); + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + root.__activateNext(); + break; + case "ArrowUp": + event.preventDefault(); + root.__activatePrev(); + break; + case "Home": + event.preventDefault(); + root.__activateFirst(); + break; + case "End": + event.preventDefault(); + root.__activateLast(); + break; + case "Escape": + event.preventDefault(); + root.__deactivate(); + break; + case "Tab": + root.__deactivate(); + break; + } + }); + } }, get all() { let allOptions = root.__optionsEls; @@ -403,12 +445,12 @@ __filteredValues: null, __prevVisibleArray: null, __prevActiveValue: void 0, - __effectRAF: NaN, + __effectRAF: null, __inputManager: void 0, __optionsManager: void 0, __optionManager: void 0, __buttonManager: void 0, - __add: (value, disabled) => collection.add(value, disabled), + __add: (value, search, disabled) => collection.add(value, search, disabled), __forget: (value) => collection.forget(value), __activate: (value) => collection.activate(value), __deactivate: () => collection.deactivate(), @@ -426,7 +468,6 @@ this.__isLoading = collection.pending.state; }); this.$watch("_x__searchQuery", (query) => { - this.__isLoading = true; if (query.length > 0) { const results = this.__searchUsingQuery(query).map((r) => r.value); const prev = this.__filteredValues; @@ -442,10 +483,9 @@ if (this.__activatedValue && this.__filteredValues && !this.__filteredValues.includes(this.__activatedValue)) { this.__deactivate(); } - if (this.__isOpen && !this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { + if (!this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { this.__activate(this.__filteredValues[0]); } - this.__isLoading = false; }); this.$nextTick(() => { this.__optionsEls = Array.from(this.$el.querySelectorAll("[x-rover\\:option]")); @@ -459,14 +499,11 @@ const activeItem = this.__getByIndex(collection.activeIndex.value); const activeValue = this.__activatedValue = activeItem?.value; const visibleValuesArray = this.__filteredValues; - if (!Number.isNaN(this.__effectRAF)) - cancelAnimationFrame(this.__effectRAF); - this.__effectRAF = requestAnimationFrame(() => { + requestAnimationFrame(() => { this.__patchItemsVisibility(visibleValuesArray); this.__patchItemsActivity(activeValue); this.__handleSeparatorsVisibility(); this.__handleGroupsVisibility(); - this.__effectRAF = null; }); }); }); @@ -577,8 +614,6 @@ return ++this.__s_id; }, destroy() { - if (this.__effectRAF) - cancelAnimationFrame(this.__effectRAF); this.__inputManager?.destroy(); this.__optionManager?.destroy(); this.__optionsManager?.destroy(); @@ -609,6 +644,14 @@ get isLoading() { return data.__isLoading; }, + get inputEl() { + return data.$root.querySelector("[x-rover\\:input]"); + }, + reindex() { + }, + getOptionElByValue(value) { + return data.__optionIndex?.get(value); + }, activate(key) { data.__collection.activate(key); }, @@ -752,7 +795,6 @@ } function handleInput(Alpine3, el) { Alpine3.bind(el, { - "x-ref": "_x__input", "x-model": "_x__searchQuery", "x-bind:id"() { return this.$id("rover-input"); diff --git a/dist/cdn.min.js b/dist/cdn.min.js index 4676513..4eebccd 100644 --- a/dist/cdn.min.js +++ b/dist/cdn.min.js @@ -1 +1 @@ -(()=>{function _(n){return{init(){let e=n.extractProp(this.$el,"disabled",!1,!1),t=n.extractProp(this.$el,"value","");this.$el.dataset.value=t,this.__add(t,e),this.$nextTick(()=>{e&&this.$el.setAttribute("tabindex","-1")})},destroy(){this.__forget(this.__uniqueKey)}}}var h=class{constructor(e={}){this.items=[];this.currentQuery="";this.currentResults=[];this.navIndex=[];this.activeNavPos=-1;this.needsReindex=!1;this.isProcessing=!1;this.pending=Alpine.reactive({state:!1}),this.activeIndex=Alpine.reactive({value:void 0}),this.searchThreshold=e.searchThreshold??500}add(e,t=!1){let i={value:e,disabled:t};this.items.push(i),this.invalidate()}forget(e){let t=this.items.findIndex(i=>i.value===e);t!==-1&&(this.items.splice(t,1),this.activeIndex.value===t?(this.activeIndex.value=void 0,this.activeNavPos=-1):this.activeIndex.value!==void 0&&this.activeIndex.value>t&&this.activeIndex.value--,this.invalidate())}invalidate(){this.needsReindex=!0,this.currentQuery="",this.currentResults=[],this.scheduleBatchAsANextMicroTask()}scheduleBatchAsANextMicroTask(){this.isProcessing||(this.isProcessing=!0,this.pending.state=!0,queueMicrotask(()=>{this.rebuildNavIndex(),this.isProcessing=!1,this.pending.state=!1}))}rebuildNavIndex(){this.navIndex=[];let e=this.currentResults.length>0?this.currentResults:this.items;for(let t=0;t0){let r=this.currentResults.filter(s=>String(s.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").includes(t));return this.currentQuery=t,this.currentResults=r,this.rebuildNavIndex(),r}let i=this.items.filter(r=>String(r.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").includes(t));return this.currentQuery=t,this.currentResults=i,this.rebuildNavIndex(),i}get(e){return this.items.find(t=>t.value===e)}getByIndex(e){return e==null||e===void 0?null:this.items[e]??null}all(){return this.items}get size(){return this.items.length}activate(e){let t=this.items.findIndex(r=>r.value===e);t===-1||this.items[t]?.disabled||(this.rebuildNavIndex(),this.activeIndex.value!==t&&(this.activeIndex.value=t,this.activeNavPos=this.navIndex.indexOf(t)))}deactivate(){this.activeIndex.value=void 0,this.activeNavPos=-1}isActivated(e){let t=this.items.findIndex(i=>i.value===e);return t===-1?!1:t===this.activeIndex.value}getActiveItem(){return this.activeIndex.value===void 0?null:this.items[this.activeIndex.value]??null}activateFirst(){if(this.rebuildNavIndex(),!this.navIndex.length)return;let e=this.navIndex[0];e!==void 0&&(this.activeIndex.value=e,this.activeNavPos=0)}activateLast(){if(this.rebuildNavIndex(),!this.navIndex.length)return;this.activeNavPos=this.navIndex.length-1;let e=this.navIndex[this.activeNavPos];typeof e=="number"&&(this.activeIndex.value=e)}activateNext(){if(this.rebuildNavIndex(),!this.navIndex.length)return;if(this.activeNavPos===-1){this.activateFirst();return}this.activeNavPos=(this.activeNavPos+1)%this.navIndex.length;let e=this.navIndex[this.activeNavPos];e!==void 0&&(this.activeIndex.value=e)}activatePrev(){if(this.rebuildNavIndex(),!this.navIndex.length)return;if(this.activeNavPos===-1){this.activateLast();return}this.activeNavPos=this.activeNavPos===0?this.navIndex.length-1:this.activeNavPos-1;let e=this.navIndex[this.activeNavPos];e!==void 0&&(this.activeIndex.value=e)}},x=h;function p(n,e,t,i){n.addEventListener(e,t,{signal:i.signal})}function b(n){let e=n.$el.querySelector("[x-rover\\:input]");return e||console.warn("Input element with [x-rover\\:input] not found"),{controller:new AbortController,on(t,i){if(!e)return;p(e,t,s=>{i(s,n.__activatedValue??void 0)},this.controller)},get value(){return e?e.value:""},set value(t){e&&(e.value=t)},enableDefaultInputHandlers(t=[]){!e||(t.includes("focus")||this.on("focus",()=>n.__startTyping()),t.includes("blur")||this.on("blur",()=>n.__stopTyping()),t.includes("keydown")||this.on("keydown",i=>{switch(i.key){case"ArrowDown":i.preventDefault(),i.stopPropagation(),n.__activateNext();break;case"ArrowUp":i.preventDefault(),i.stopPropagation(),n.__activatePrev();break;case"Escape":i.preventDefault(),i.stopPropagation(),requestAnimationFrame(()=>e?.focus({preventScroll:!0}));break}}))},destroy(){this.controller.abort()}}}function y(n){let e=()=>Array.from(n.$el.querySelectorAll("[x-rover\\:option]"));return{controller:new AbortController,on(t,i){n.$nextTick(()=>{let r=e();if(!r)return;let s=a=>{i(a,n.__activatedValue??void 0)};r.forEach(a=>{p(a,t,s,this.controller)})})},destroy(){this.controller.abort()}}}function A(n){let e=n.$el.querySelector("[x-rover\\:options]");e||console.warn("Options container not found");let t=i=>{if(!(!i||!(i instanceof HTMLElement)))return Alpine.findClosest(i,r=>r.hasAttribute("x-rover:option"))};return{controller:new AbortController,on(i,r){if(!e)return;p(e,i,a=>{let u=t(a.target);r(a,u,n.__activatedValue??null)},this.controller)},findClosestOption:t,enableDefaultOptionsHandlers(i=[]){Object.entries({click:s=>{!s.dataset.value||n.$nextTick(()=>n.$refs.__input?.focus({preventScroll:!0}))},mouseover:s=>{!s.dataset.value||n.__activate(s.dataset.value)},mousemove:s=>{!s.dataset.value||n.__isActive(s.dataset.value)||n.__activate(s.dataset.value)},mouseout:()=>{n.__keepActivated||n.__deactivate()}}).forEach(([s,a])=>{i.includes(s)||this.on(s,(u,v)=>{u.stopPropagation(),!!v&&a(v)})})},get all(){let i=n.__optionsEls;return i?Array.from(i):[]},destroy(){this.controller.abort()}}}function I(n){let e=n.$el.querySelector("[x-rover\\:button]");return{controller:new AbortController,on(t,i){if(!e)return;p(e,t,s=>{i(s,n.__activatedValue??void 0)},this.controller)},destroy(){this.controller.abort()}}}function f({effect:n}){let e=new x;return{__collection:e,__optionsEls:void 0,__groupsEls:void 0,__optionIndex:void 0,__isOpen:!1,__isTyping:!1,__isLoading:!1,__g_id:-1,__s_id:-1,__static:!1,__keepActivated:!0,__optionsEl:void 0,__prevActivatedValue:void 0,__activatedValue:void 0,__items:[],_x__searchQuery:"",__filteredValues:null,__prevVisibleArray:null,__prevActiveValue:void 0,__effectRAF:NaN,__inputManager:void 0,__optionsManager:void 0,__optionManager:void 0,__buttonManager:void 0,__add:(t,i)=>e.add(t,i),__forget:t=>e.forget(t),__activate:t=>e.activate(t),__deactivate:()=>e.deactivate(),__isActive:t=>e.isActivated(t),__getActiveItem:()=>e.getActiveItem(),__activateNext:()=>e.activateNext(),__activatePrev:()=>e.activatePrev(),__activateFirst:()=>e.activateFirst(),__activateLast:()=>e.activateLast(),__searchUsingQuery:t=>e.search(t),__getByIndex:t=>e.getByIndex(t),init(){this.__setupManagers(),n(()=>{this.__isLoading=e.pending.state}),this.$watch("_x__searchQuery",t=>{if(this.__isLoading=!0,t.length>0){let i=this.__searchUsingQuery(t).map(a=>a.value),r=this.__filteredValues;(!r||r.length!==i.length||i.some((a,u)=>a!==r[u]))&&(this.__filteredValues=i)}else this.__filteredValues!==null&&(this.__filteredValues=null);this.__activatedValue&&this.__filteredValues&&!this.__filteredValues.includes(this.__activatedValue)&&this.__deactivate(),this.__isOpen&&!this.__getActiveItem()&&this.__filteredValues&&this.__filteredValues.length&&this.__activate(this.__filteredValues[0]),this.__isLoading=!1}),this.$nextTick(()=>{this.__optionsEls=Array.from(this.$el.querySelectorAll("[x-rover\\:option]")),this.__optionIndex=new Map,this.__optionsEls.forEach(t=>{let i=t.dataset.value;i&&this.__optionIndex.set(i,t)}),n(()=>{let t=this.__getByIndex(e.activeIndex.value),i=this.__activatedValue=t?.value,r=this.__filteredValues;Number.isNaN(this.__effectRAF)||cancelAnimationFrame(this.__effectRAF),this.__effectRAF=requestAnimationFrame(()=>{this.__patchItemsVisibility(r),this.__patchItemsActivity(i),this.__handleSeparatorsVisibility(),this.__handleGroupsVisibility(),this.__effectRAF=null})})})},__handleGroupsVisibility(){},__handleSeparatorsVisibility(){},__patchItemsVisibility(t){if(!this.__optionsEls||!this.__optionIndex)return;let i=this.__prevVisibleArray;if(t===i)return;if(t===null){if(i===null)return;this.__optionsEls.forEach(a=>{a.style.display=""}),this.__prevVisibleArray=null;return}let r=new Set(t),s=i?new Set(i):null;if(s===null){this.__optionsEls.forEach(a=>{let u=a.dataset.value;if(!u)return;let v=!r.has(u);a.style.display!==(v?"none":"")&&(a.style.display=v?"none":"")}),this.__prevVisibleArray=t;return}for(let a of s)if(!r.has(a)){let u=this.__optionIndex.get(a);u&&(u.style.display="none")}for(let a of r)if(!s.has(a)){let u=this.__optionIndex.get(a);u&&(u.style.display="")}this.__prevVisibleArray=t},__patchItemsActivity(t){let i=this.__prevActiveValue;if(i!==t){if(i){let r=this.__optionIndex.get(i);r&&(r.removeAttribute("data-active"),r.removeAttribute("aria-current"))}if(t){let r=this.__optionIndex.get(t);r&&(r.setAttribute("data-active","true"),r.setAttribute("aria-current","true"),requestAnimationFrame(()=>{r.scrollIntoView({block:"nearest"})}))}this.__prevActiveValue=t}},__setupManagers(){this.__inputManager=b(this),this.__optionManager=y(this),this.__optionsManager=A(this),this.__buttonManager=I(this)},__open(){this.__isOpen||(this.__isOpen=!0,requestAnimationFrame(()=>{this.$refs?._x__input?.focus({preventScroll:!0})}))},__pushSeparatorToItems(t){this.__items.push({type:"s",id:t})},__pushGroupToItems(t){this.__items.push({type:"g",id:t})},__startTyping(){this.__isTyping=!0},__stopTyping(){this.__isTyping=!1},__nextGroupId(){return++this.__g_id},__nextSeparatorId(){return++this.__s_id},destroy(){this.__effectRAF&&cancelAnimationFrame(this.__effectRAF),this.__inputManager?.destroy(),this.__optionManager?.destroy(),this.__optionsManager?.destroy(),this.__buttonManager?.destroy()}}}var M=n=>{let e=Alpine.$data(n);return{get collection(){return e.__collection},get input(){return e.__inputManager},get option(){return e.__optionManager},get options(){return e.__optionsManager},get button(){return e.__buttonManager},get isLoading(){return e.__isLoading},activate(t){e.__collection.activate(t)},deactivate(){e.__collection.deactivate()},getActiveItem(){return e.__collection.getActiveItem()},activateNext(){e.__collection.activateNext()},activatePrev(){e.__collection.activatePrev()},activateFirst(){e.__collection.activateFirst()},activateLast(){e.__collection.activateLast()},searchUsing(t){return e.__collection.search(t)}}};var E=n=>({activate(e){n.__collection.activate(e)},deactivate(){n.__collection.deactivate()},getActiveItem(){return n.__collection.getActiveItem()},activateNext(){n.__collection.activateNext()},activatePrev(){n.__collection.activatePrev()},activateFirst(){n.__collection.activateFirst()},activateLast(){n.__collection.activateLast()},searchUsing(e){return n.__collection.search(e)}});var R=n=>({isOpen(){return n.__isOpen},isStatic(){return n.__static}});function m(n){n.magic("rover",e=>{let t=n.findClosest(e,i=>i.hasAttribute("x-rover"));if(!t)throw"No x-rover directive found, this magic meant to be used under x-rover root context...";return M(t)}),n.magic("roverOption",e=>{let t=n.findClosest(e,r=>r.hasAttribute("x-rover:option"));if(!t)throw"No x-rover:option directive found, this magic meant to be used per option context...";let i=n.$data(t);return E(i)}),n.magic("roverOptions",e=>{let t=n.findClosest(e,r=>r.hasAttribute("x-option:options"));if(!t)throw"No x-rover:options directive found, this magic meant to be used per options context...";let i=n.$data(t);return R(i)})}function g(n){n.directive("rover",(o,{value:l,modifiers:c},{Alpine:d,effect:L})=>{switch(l){case null:e(d,o,L);break;case"input":t(d,o);break;case"button":a(d,o);break;case"options":i(o);break;case"option":r(d,o);break;case"group":s(d,o);break;case"loading":v(d,o,c);break;case"separator":T(d,o);break;case"empty":u(d,o);break;default:console.error("invalid x-rover value",l,"use input, button, option, options, group or leave mepty for root level instead");break}}).before("data"),m(n);function e(o,l,c){o.bind(l,{"x-data"(){return{...f({effect:c})}}})}function t(o,l){o.bind(l,{"x-ref":"_x__input","x-model":"_x__searchQuery","x-bind:id"(){return this.$id("rover-input")},role:"combobox",tabindex:"0","aria-autocomplete":"list"})}function i(o){n.bind(o,{"x-ref":"__options","x-bind:id"(){return this.$id("rover-options")},role:"listbox","x-init"(){n.bound(this.$el,"keepActivated")&&(this.__keepActivated=!0)},"x-bind:data-loading"(){return this.__isLoading}})}function r(o,l){o.bind(l,{"x-id"(){return["rover-option"]},"x-bind:id"(){return this.$id("rover-option")},role:"option","x-data"(){return _(o)}})}function s(o,l){o.bind(l,{"x-id"(){return["rover-group"]},"x-bind:id"(){return this.$id("rover-group")},role:"group","x-init"(){let c=this.$id("rover-group");this.$el.setAttribute("aria-labelledby",`${c}-label`)}})}function a(o,l){o.bind(l,{"x-ref":"__button","x-bind:id"(){return this.$id("rover-button")},tabindex:"-1","aria-haspopup":"true"})}function u(o,l){o.bind(l,{"x-bind:id"(){return this.$id("rover-button")},tabindex:"-1","aria-haspopup":"true","x-show"(){return Array.isArray(this.__filteredValues)&&this.__filteredValues.length===0&&this._x__searchQuery.length>0}})}function v(o,l,c){let d=c.includes("hide");o.bind(l,{"x-show"(){return d?!this.__isLoading:this.__isLoading},role:"status","aria-live":"polite","aria-atomic":"true"})}function T(o,l){o.bind(l,{"x-init"(){let c=String(this.__nextSeparatorId());this.$el.dataset.key=c,this.__pushSeparatorToItems(c),this.$el.setAttribute("role","separator"),this.$el.setAttribute("aria-orientation","horizontal")}})}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})(); +(()=>{function _(i){return{init(){let e=i.extractProp(this.$el,"disabled",!1,!1),t=i.extractProp(this.$el,"value",""),n=i.extractProp(this.$el,"data-search",t),r=String(n).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g,"").trim();this.$el.dataset.value=t,this.__add(t,r,e),this.$nextTick(()=>{e&&this.$el.setAttribute("tabindex","-1")})},destroy(){this.__forget(this.__uniqueKey)}}}var h=class{constructor(e={}){this.items=[];this.currentQuery="";this.currentResults=[];this.navIndex=[];this.activeNavPos=-1;this.needsReindex=!1;this.isProcessing=!1;this.pending=Alpine.reactive({state:!1}),this.activeIndex=Alpine.reactive({value:void 0}),this.searchThreshold=e.searchThreshold??500}add(e,t,n=!1){let r={value:e,disabled:n,searchable:t};this.items.push(r),this.invalidate()}forget(e){let t=this.items.findIndex(n=>n.value===e);t!==-1&&(this.items.splice(t,1),this.activeIndex.value===t?(this.activeIndex.value=void 0,this.activeNavPos=-1):this.activeIndex.value!==void 0&&this.activeIndex.value>t&&this.activeIndex.value--,this.invalidate())}invalidate(){this.needsReindex=!0,this.currentQuery="",this.currentResults=[],this.scheduleBatchAsANextMicroTask()}scheduleBatchAsANextMicroTask(){this.isProcessing||(this.isProcessing=!0,this.pending.state=!0,queueMicrotask(()=>{this.rebuildNavIndex(),this.isProcessing=!1,this.pending.state=!1}))}rebuildNavIndex(){this.navIndex=[];let e=this.currentResults.length>0?this.currentResults:this.items;for(let t=0;t0){let r=this.currentResults.filter(a=>a.searchable.includes(t));return this.currentQuery=t,this.currentResults=r,this.rebuildNavIndex(),r}let n=this.items.filter(r=>r.searchable.includes(t));return this.currentQuery=t,this.currentResults=n,this.rebuildNavIndex(),n}get(e){return this.items.find(t=>t.value===e)}getByIndex(e){return e==null||e===void 0?null:this.items[e]??null}all(){return this.items}get size(){return this.items.length}activate(e){let t=this.items.findIndex(r=>r.value===e);t===-1||this.items[t]?.disabled||(this.rebuildNavIndex(),this.activeIndex.value!==t&&(this.activeIndex.value=t,this.activeNavPos=this.navIndex.indexOf(t)))}deactivate(){this.activeIndex.value=void 0,this.activeNavPos=-1}isActivated(e){let t=this.items.findIndex(n=>n.value===e);return t===-1?!1:t===this.activeIndex.value}getActiveItem(){return this.activeIndex.value===void 0?null:this.items[this.activeIndex.value]??null}activateFirst(){if(this.rebuildNavIndex(),!this.navIndex.length)return;let e=this.navIndex[0];e!==void 0&&(this.activeIndex.value=e,this.activeNavPos=0)}activateLast(){if(this.rebuildNavIndex(),!this.navIndex.length)return;this.activeNavPos=this.navIndex.length-1;let e=this.navIndex[this.activeNavPos];typeof e=="number"&&(this.activeIndex.value=e)}activateNext(){if(this.rebuildNavIndex(),!this.navIndex.length)return;if(this.activeNavPos===-1){this.activateFirst();return}this.activeNavPos=(this.activeNavPos+1)%this.navIndex.length;let e=this.navIndex[this.activeNavPos];e!==void 0&&(this.activeIndex.value=e)}activatePrev(){if(this.rebuildNavIndex(),!this.navIndex.length)return;if(this.activeNavPos===-1){this.activateLast();return}this.activeNavPos=this.activeNavPos===0?this.navIndex.length-1:this.activeNavPos-1;let e=this.navIndex[this.activeNavPos];e!==void 0&&(this.activeIndex.value=e)}},x=h;function p(i,e,t,n){i.addEventListener(e,t,{signal:n.signal})}function b(i){let e=i.$root.querySelector("[x-rover\\:input]"),t=()=>e?!0:(console.warn("Input element with [x-rover\\:input] not found"),!1);return{controller:new AbortController,on(n,r){if(!t())return;p(e,n,o=>{r(o,i.__activatedValue??void 0)},this.controller)},get value(){return e?e.value:""},set value(n){e&&(e.value=n)},focus(n=!0){requestAnimationFrame(()=>e?.focus({preventScroll:n}))},enableDefaultInputHandlers(n=[]){!t()||(n.includes("focus")||this.on("focus",()=>i.__startTyping()),n.includes("blur")||this.on("blur",()=>i.__stopTyping()),n.includes("keydown")||this.on("keydown",r=>{switch(r.key){case"ArrowDown":r.preventDefault(),r.stopPropagation(),i.__activateNext();break;case"ArrowUp":r.preventDefault(),r.stopPropagation(),i.__activatePrev();break;case"Escape":r.preventDefault(),r.stopPropagation(),requestAnimationFrame(()=>this.focus(!0));break;case"Home":r.preventDefault(),i.__activateFirst();break;case"End":r.preventDefault(),i.__activateLast();break;case"Tab":i.__stopTyping();break}}))},destroy(){this.controller.abort()}}}function y(i){let e=()=>Array.from(i.$el.querySelectorAll("[x-rover\\:option]"));return{controller:new AbortController,on(t,n){i.$nextTick(()=>{let r=e();if(!r)return;let a=o=>{n(o,i.__activatedValue??void 0)};r.forEach(o=>{p(o,t,a,this.controller)})})},destroy(){this.controller.abort()}}}function I(i){let e=i.$el.querySelector("[x-rover\\:options]");e||console.warn("Options container not found");let t=n=>{if(!(!n||!(n instanceof HTMLElement)))return Alpine.findClosest(n,r=>r.hasAttribute("x-rover:option"))};return{controller:new AbortController,on(n,r){if(!e)return;p(e,n,o=>{let u=t(o.target);r(o,u,i.__activatedValue??null)},this.controller)},findClosestOption:t,enableDefaultOptionsHandlers(n=[]){!e||(e.tabIndex=0,n.includes("mouseover")||this.on("mouseover",(r,a)=>{!a?.dataset.value||i.__activate(a.dataset.value)}),n.includes("mousemove")||this.on("mousemove",(r,a)=>{!a?.dataset.value||i.__isActive(a.dataset.value)||i.__activate(a.dataset.value)}),n.includes("mouseout")||this.on("mouseout",()=>{i.__keepActivated||i.__deactivate()}),n.includes("keydown")||this.on("keydown",r=>{switch(r.stopPropagation(),r.key){case"ArrowDown":r.preventDefault(),i.__activateNext();break;case"ArrowUp":r.preventDefault(),i.__activatePrev();break;case"Home":r.preventDefault(),i.__activateFirst();break;case"End":r.preventDefault(),i.__activateLast();break;case"Escape":r.preventDefault(),i.__deactivate();break;case"Tab":i.__deactivate();break}}))},get all(){let n=i.__optionsEls;return n?Array.from(n):[]},destroy(){this.controller.abort()}}}function A(i){let e=i.$el.querySelector("[x-rover\\:button]");return{controller:new AbortController,on(t,n){if(!e)return;p(e,t,a=>{n(a,i.__activatedValue??void 0)},this.controller)},destroy(){this.controller.abort()}}}function f({effect:i}){let e=new x;return{__collection:e,__optionsEls:void 0,__groupsEls:void 0,__optionIndex:void 0,__isOpen:!1,__isTyping:!1,__isLoading:!1,__g_id:-1,__s_id:-1,__static:!1,__keepActivated:!0,__optionsEl:void 0,__prevActivatedValue:void 0,__activatedValue:void 0,__items:[],_x__searchQuery:"",__filteredValues:null,__prevVisibleArray:null,__prevActiveValue:void 0,__effectRAF:null,__inputManager:void 0,__optionsManager:void 0,__optionManager:void 0,__buttonManager:void 0,__add:(t,n,r)=>e.add(t,n,r),__forget:t=>e.forget(t),__activate:t=>e.activate(t),__deactivate:()=>e.deactivate(),__isActive:t=>e.isActivated(t),__getActiveItem:()=>e.getActiveItem(),__activateNext:()=>e.activateNext(),__activatePrev:()=>e.activatePrev(),__activateFirst:()=>e.activateFirst(),__activateLast:()=>e.activateLast(),__searchUsingQuery:t=>e.search(t),__getByIndex:t=>e.getByIndex(t),init(){this.__setupManagers(),i(()=>{this.__isLoading=e.pending.state}),this.$watch("_x__searchQuery",t=>{if(t.length>0){let n=this.__searchUsingQuery(t).map(o=>o.value),r=this.__filteredValues;(!r||r.length!==n.length||n.some((o,u)=>o!==r[u]))&&(this.__filteredValues=n)}else this.__filteredValues!==null&&(this.__filteredValues=null);this.__activatedValue&&this.__filteredValues&&!this.__filteredValues.includes(this.__activatedValue)&&this.__deactivate(),!this.__getActiveItem()&&this.__filteredValues&&this.__filteredValues.length&&this.__activate(this.__filteredValues[0])}),this.$nextTick(()=>{this.__optionsEls=Array.from(this.$el.querySelectorAll("[x-rover\\:option]")),this.__optionIndex=new Map,this.__optionsEls.forEach(t=>{let n=t.dataset.value;n&&this.__optionIndex.set(n,t)}),i(()=>{let t=this.__getByIndex(e.activeIndex.value),n=this.__activatedValue=t?.value,r=this.__filteredValues;requestAnimationFrame(()=>{this.__patchItemsVisibility(r),this.__patchItemsActivity(n),this.__handleSeparatorsVisibility(),this.__handleGroupsVisibility()})})})},__handleGroupsVisibility(){},__handleSeparatorsVisibility(){},__patchItemsVisibility(t){if(!this.__optionsEls||!this.__optionIndex)return;let n=this.__prevVisibleArray;if(t===n)return;if(t===null){if(n===null)return;this.__optionsEls.forEach(o=>{o.style.display=""}),this.__prevVisibleArray=null;return}let r=new Set(t),a=n?new Set(n):null;if(a===null){this.__optionsEls.forEach(o=>{let u=o.dataset.value;if(!u)return;let v=!r.has(u);o.style.display!==(v?"none":"")&&(o.style.display=v?"none":"")}),this.__prevVisibleArray=t;return}for(let o of a)if(!r.has(o)){let u=this.__optionIndex.get(o);u&&(u.style.display="none")}for(let o of r)if(!a.has(o)){let u=this.__optionIndex.get(o);u&&(u.style.display="")}this.__prevVisibleArray=t},__patchItemsActivity(t){let n=this.__prevActiveValue;if(n!==t){if(n){let r=this.__optionIndex.get(n);r&&(r.removeAttribute("data-active"),r.removeAttribute("aria-current"))}if(t){let r=this.__optionIndex.get(t);r&&(r.setAttribute("data-active","true"),r.setAttribute("aria-current","true"),requestAnimationFrame(()=>{r.scrollIntoView({block:"nearest"})}))}this.__prevActiveValue=t}},__setupManagers(){this.__inputManager=b(this),this.__optionManager=y(this),this.__optionsManager=I(this),this.__buttonManager=A(this)},__open(){this.__isOpen||(this.__isOpen=!0,requestAnimationFrame(()=>{this.$refs?._x__input?.focus({preventScroll:!0})}))},__pushSeparatorToItems(t){this.__items.push({type:"s",id:t})},__pushGroupToItems(t){this.__items.push({type:"g",id:t})},__startTyping(){this.__isTyping=!0},__stopTyping(){this.__isTyping=!1},__nextGroupId(){return++this.__g_id},__nextSeparatorId(){return++this.__s_id},destroy(){this.__inputManager?.destroy(),this.__optionManager?.destroy(),this.__optionsManager?.destroy(),this.__buttonManager?.destroy()}}}var M=i=>{let e=Alpine.$data(i);return{get collection(){return e.__collection},get input(){return e.__inputManager},get option(){return e.__optionManager},get options(){return e.__optionsManager},get button(){return e.__buttonManager},get isLoading(){return e.__isLoading},get inputEl(){return e.$root.querySelector("[x-rover\\:input]")},reindex(){},getOptionElByValue(t){return e.__optionIndex?.get(t)},activate(t){e.__collection.activate(t)},deactivate(){e.__collection.deactivate()},getActiveItem(){return e.__collection.getActiveItem()},activateNext(){e.__collection.activateNext()},activatePrev(){e.__collection.activatePrev()},activateFirst(){e.__collection.activateFirst()},activateLast(){e.__collection.activateLast()},searchUsing(t){return e.__collection.search(t)}}};var E=i=>({activate(e){i.__collection.activate(e)},deactivate(){i.__collection.deactivate()},getActiveItem(){return i.__collection.getActiveItem()},activateNext(){i.__collection.activateNext()},activatePrev(){i.__collection.activatePrev()},activateFirst(){i.__collection.activateFirst()},activateLast(){i.__collection.activateLast()},searchUsing(e){return i.__collection.search(e)}});var R=i=>({isOpen(){return i.__isOpen},isStatic(){return i.__static}});function m(i){i.magic("rover",e=>{let t=i.findClosest(e,n=>n.hasAttribute("x-rover"));if(!t)throw"No x-rover directive found, this magic meant to be used under x-rover root context...";return M(t)}),i.magic("roverOption",e=>{let t=i.findClosest(e,r=>r.hasAttribute("x-rover:option"));if(!t)throw"No x-rover:option directive found, this magic meant to be used per option context...";let n=i.$data(t);return E(n)}),i.magic("roverOptions",e=>{let t=i.findClosest(e,r=>r.hasAttribute("x-option:options"));if(!t)throw"No x-rover:options directive found, this magic meant to be used per options context...";let n=i.$data(t);return R(n)})}function g(i){i.directive("rover",(s,{value:l,modifiers:c},{Alpine:d,effect:L})=>{switch(l){case null:e(d,s,L);break;case"input":t(d,s);break;case"button":o(d,s);break;case"options":n(s);break;case"option":r(d,s);break;case"group":a(d,s);break;case"loading":v(d,s,c);break;case"separator":T(d,s);break;case"empty":u(d,s);break;default:console.error("invalid x-rover value",l,"use input, button, option, options, group or leave mepty for root level instead");break}}).before("data"),m(i);function e(s,l,c){s.bind(l,{"x-data"(){return{...f({effect:c})}}})}function t(s,l){s.bind(l,{"x-model":"_x__searchQuery","x-bind:id"(){return this.$id("rover-input")},role:"combobox",tabindex:"0","aria-autocomplete":"list"})}function n(s){i.bind(s,{"x-ref":"__options","x-bind:id"(){return this.$id("rover-options")},role:"listbox","x-init"(){i.bound(this.$el,"keepActivated")&&(this.__keepActivated=!0)},"x-bind:data-loading"(){return this.__isLoading}})}function r(s,l){s.bind(l,{"x-id"(){return["rover-option"]},"x-bind:id"(){return this.$id("rover-option")},role:"option","x-data"(){return _(s)}})}function a(s,l){s.bind(l,{"x-id"(){return["rover-group"]},"x-bind:id"(){return this.$id("rover-group")},role:"group","x-init"(){let c=this.$id("rover-group");this.$el.setAttribute("aria-labelledby",`${c}-label`)}})}function o(s,l){s.bind(l,{"x-ref":"__button","x-bind:id"(){return this.$id("rover-button")},tabindex:"-1","aria-haspopup":"true"})}function u(s,l){s.bind(l,{"x-bind:id"(){return this.$id("rover-button")},tabindex:"-1","aria-haspopup":"true","x-show"(){return Array.isArray(this.__filteredValues)&&this.__filteredValues.length===0&&this._x__searchQuery.length>0}})}function v(s,l,c){let d=c.includes("hide");s.bind(l,{"x-show"(){return d?!this.__isLoading:this.__isLoading},role:"status","aria-live":"polite","aria-atomic":"true"})}function T(s,l){s.bind(l,{"x-init"(){let c=String(this.__nextSeparatorId());this.$el.dataset.key=c,this.__pushSeparatorToItems(c),this.$el.setAttribute("role","separator"),this.$el.setAttribute("aria-orientation","horizontal")}})}}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(g)});})(); diff --git a/dist/module.cjs.js b/dist/module.cjs.js index 7d31a27..bddf8fd 100644 --- a/dist/module.cjs.js +++ b/dist/module.cjs.js @@ -17,8 +17,10 @@ function CreateRoverOption(Alpine2) { init() { let disabled = Alpine2.extractProp(this.$el, "disabled", false, false); let value = Alpine2.extractProp(this.$el, "value", ""); + const rawSearch = Alpine2.extractProp(this.$el, "data-search", value); + const normalizedSearch = String(rawSearch).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); this.$el.dataset.value = value; - this.__add(value, disabled); + this.__add(value, normalizedSearch, disabled); this.$nextTick(() => { if (disabled) { this.$el.setAttribute("tabindex", "-1"); @@ -46,8 +48,8 @@ var RoverCollection = class { this.activeIndex = Alpine.reactive({value: void 0}); this.searchThreshold = (_a = options.searchThreshold) != null ? _a : 500; } - add(value, disabled = false) { - const item = {value, disabled}; + add(value, searchable, disabled = false) { + const item = {value, disabled, searchable}; this.items.push(item); this.invalidate(); } @@ -101,22 +103,20 @@ var RoverCollection = class { this.rebuildNavIndex(); return this.items; } - const normalized = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - if (this.currentQuery && normalized.startsWith(this.currentQuery) && this.currentResults.length > 0) { + const normalizedQuery = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + if (this.currentQuery && normalizedQuery.startsWith(this.currentQuery) && this.currentResults.length > 0) { const filtered = this.currentResults.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = filtered; this.rebuildNavIndex(); return filtered; } const results = this.items.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = results; this.rebuildNavIndex(); return results; @@ -225,14 +225,18 @@ function bindListener(el, eventKey, listener, controller) { // src/Managers/InputManager.ts function createInputManager(rootDataStack) { - const inputEl = rootDataStack.$el.querySelector("[x-rover\\:input]"); - if (!inputEl) { - console.warn(`Input element with [x-rover\\:input] not found`); - } + const inputEl = rootDataStack.$root.querySelector("[x-rover\\:input]"); + const inputElExists = () => { + if (!inputEl) { + console.warn(`Input element with [x-rover\\:input] not found`); + return false; + } + return true; + }; return { controller: new AbortController(), on(eventKey, handler) { - if (!inputEl) + if (!inputElExists()) return; const listener = (event) => { var _a; @@ -247,8 +251,11 @@ function createInputManager(rootDataStack) { if (inputEl) inputEl.value = val; }, + focus(preventScroll = true) { + requestAnimationFrame(() => inputEl == null ? void 0 : inputEl.focus({preventScroll})); + }, enableDefaultInputHandlers(disabledEvents = []) { - if (!inputEl) + if (!inputElExists()) return; if (!disabledEvents.includes("focus")) { this.on("focus", () => rootDataStack.__startTyping()); @@ -272,7 +279,18 @@ function createInputManager(rootDataStack) { case "Escape": e.preventDefault(); e.stopPropagation(); - requestAnimationFrame(() => inputEl == null ? void 0 : inputEl.focus({preventScroll: true})); + requestAnimationFrame(() => this.focus(true)); + break; + case "Home": + e.preventDefault(); + rootDataStack.__activateFirst(); + break; + case "End": + e.preventDefault(); + rootDataStack.__activateLast(); + break; + case "Tab": + rootDataStack.__stopTyping(); break; } }); @@ -333,41 +351,62 @@ function createOptionsManager(root) { }, findClosestOption, enableDefaultOptionsHandlers(disabledEvents = []) { - const events = { - click: (optionEl) => { - if (!optionEl.dataset.value) - return; - root.$nextTick(() => { - var _a; - return (_a = root.$refs.__input) == null ? void 0 : _a.focus({preventScroll: true}); - }); - }, - mouseover: (optionEl) => { - if (!optionEl.dataset.value) + if (!optionsEl) + return; + optionsEl.tabIndex = 0; + if (!disabledEvents.includes("mouseover")) { + this.on("mouseover", (_event, optionEl) => { + if (!(optionEl == null ? void 0 : optionEl.dataset.value)) return; root.__activate(optionEl.dataset.value); - }, - mousemove: (optionEl) => { - if (!optionEl.dataset.value || root.__isActive(optionEl.dataset.value)) + }); + } + if (!disabledEvents.includes("mousemove")) { + this.on("mousemove", (_event, optionEl) => { + if (!(optionEl == null ? void 0 : optionEl.dataset.value)) + return; + if (root.__isActive(optionEl.dataset.value)) return; root.__activate(optionEl.dataset.value); - }, - mouseout: () => { + }); + } + if (!disabledEvents.includes("mouseout")) { + this.on("mouseout", () => { if (root.__keepActivated) return; root.__deactivate(); - } - }; - Object.entries(events).forEach(([key, handler]) => { - if (!disabledEvents.includes(key)) { - this.on(key, (event, optionEl) => { - event.stopPropagation(); - if (!optionEl) - return; - handler(optionEl); - }); - } - }); + }); + } + if (!disabledEvents.includes("keydown")) { + this.on("keydown", (event) => { + event.stopPropagation(); + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + root.__activateNext(); + break; + case "ArrowUp": + event.preventDefault(); + root.__activatePrev(); + break; + case "Home": + event.preventDefault(); + root.__activateFirst(); + break; + case "End": + event.preventDefault(); + root.__activateLast(); + break; + case "Escape": + event.preventDefault(); + root.__deactivate(); + break; + case "Tab": + root.__deactivate(); + break; + } + }); + } }, get all() { let allOptions = root.__optionsEls; @@ -426,12 +465,12 @@ function CreateRoverRoot({ __filteredValues: null, __prevVisibleArray: null, __prevActiveValue: void 0, - __effectRAF: NaN, + __effectRAF: null, __inputManager: void 0, __optionsManager: void 0, __optionManager: void 0, __buttonManager: void 0, - __add: (value, disabled) => collection.add(value, disabled), + __add: (value, search, disabled) => collection.add(value, search, disabled), __forget: (value) => collection.forget(value), __activate: (value) => collection.activate(value), __deactivate: () => collection.deactivate(), @@ -449,7 +488,6 @@ function CreateRoverRoot({ this.__isLoading = collection.pending.state; }); this.$watch("_x__searchQuery", (query) => { - this.__isLoading = true; if (query.length > 0) { const results = this.__searchUsingQuery(query).map((r) => r.value); const prev = this.__filteredValues; @@ -465,10 +503,9 @@ function CreateRoverRoot({ if (this.__activatedValue && this.__filteredValues && !this.__filteredValues.includes(this.__activatedValue)) { this.__deactivate(); } - if (this.__isOpen && !this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { + if (!this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { this.__activate(this.__filteredValues[0]); } - this.__isLoading = false; }); this.$nextTick(() => { this.__optionsEls = Array.from(this.$el.querySelectorAll("[x-rover\\:option]")); @@ -482,14 +519,11 @@ function CreateRoverRoot({ const activeItem = this.__getByIndex(collection.activeIndex.value); const activeValue = this.__activatedValue = activeItem == null ? void 0 : activeItem.value; const visibleValuesArray = this.__filteredValues; - if (!Number.isNaN(this.__effectRAF)) - cancelAnimationFrame(this.__effectRAF); - this.__effectRAF = requestAnimationFrame(() => { + requestAnimationFrame(() => { this.__patchItemsVisibility(visibleValuesArray); this.__patchItemsActivity(activeValue); this.__handleSeparatorsVisibility(); this.__handleGroupsVisibility(); - this.__effectRAF = null; }); }); }); @@ -602,8 +636,6 @@ function CreateRoverRoot({ }, destroy() { var _a, _b, _c, _d; - if (this.__effectRAF) - cancelAnimationFrame(this.__effectRAF); (_a = this.__inputManager) == null ? void 0 : _a.destroy(); (_b = this.__optionManager) == null ? void 0 : _b.destroy(); (_c = this.__optionsManager) == null ? void 0 : _c.destroy(); @@ -634,6 +666,15 @@ var rover = (el) => { get isLoading() { return data.__isLoading; }, + get inputEl() { + return data.$root.querySelector("[x-rover\\:input]"); + }, + reindex() { + }, + getOptionElByValue(value) { + var _a; + return (_a = data.__optionIndex) == null ? void 0 : _a.get(value); + }, activate(key) { data.__collection.activate(key); }, @@ -777,7 +818,6 @@ function rover2(Alpine2) { } function handleInput(Alpine3, el) { Alpine3.bind(el, { - "x-ref": "_x__input", "x-model": "_x__searchQuery", "x-bind:id"() { return this.$id("rover-input"); diff --git a/dist/module.esm.js b/dist/module.esm.js index cf56fff..b8a8df4 100644 --- a/dist/module.esm.js +++ b/dist/module.esm.js @@ -4,8 +4,10 @@ function CreateRoverOption(Alpine2) { init() { let disabled = Alpine2.extractProp(this.$el, "disabled", false, false); let value = Alpine2.extractProp(this.$el, "value", ""); + const rawSearch = Alpine2.extractProp(this.$el, "data-search", value); + const normalizedSearch = String(rawSearch).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim(); this.$el.dataset.value = value; - this.__add(value, disabled); + this.__add(value, normalizedSearch, disabled); this.$nextTick(() => { if (disabled) { this.$el.setAttribute("tabindex", "-1"); @@ -32,8 +34,8 @@ var RoverCollection = class { this.activeIndex = Alpine.reactive({value: void 0}); this.searchThreshold = options.searchThreshold ?? 500; } - add(value, disabled = false) { - const item = {value, disabled}; + add(value, searchable, disabled = false) { + const item = {value, disabled, searchable}; this.items.push(item); this.invalidate(); } @@ -86,22 +88,20 @@ var RoverCollection = class { this.rebuildNavIndex(); return this.items; } - const normalized = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - if (this.currentQuery && normalized.startsWith(this.currentQuery) && this.currentResults.length > 0) { + const normalizedQuery = query.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + if (this.currentQuery && normalizedQuery.startsWith(this.currentQuery) && this.currentResults.length > 0) { const filtered = this.currentResults.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = filtered; this.rebuildNavIndex(); return filtered; } const results = this.items.filter((item) => { - const itemNormalized = String(item.value).toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = results; this.rebuildNavIndex(); return results; @@ -208,14 +208,18 @@ function bindListener(el, eventKey, listener, controller) { // src/Managers/InputManager.ts function createInputManager(rootDataStack) { - const inputEl = rootDataStack.$el.querySelector("[x-rover\\:input]"); - if (!inputEl) { - console.warn(`Input element with [x-rover\\:input] not found`); - } + const inputEl = rootDataStack.$root.querySelector("[x-rover\\:input]"); + const inputElExists = () => { + if (!inputEl) { + console.warn(`Input element with [x-rover\\:input] not found`); + return false; + } + return true; + }; return { controller: new AbortController(), on(eventKey, handler) { - if (!inputEl) + if (!inputElExists()) return; const listener = (event) => { handler(event, rootDataStack.__activatedValue ?? void 0); @@ -229,8 +233,11 @@ function createInputManager(rootDataStack) { if (inputEl) inputEl.value = val; }, + focus(preventScroll = true) { + requestAnimationFrame(() => inputEl?.focus({preventScroll})); + }, enableDefaultInputHandlers(disabledEvents = []) { - if (!inputEl) + if (!inputElExists()) return; if (!disabledEvents.includes("focus")) { this.on("focus", () => rootDataStack.__startTyping()); @@ -254,7 +261,18 @@ function createInputManager(rootDataStack) { case "Escape": e.preventDefault(); e.stopPropagation(); - requestAnimationFrame(() => inputEl?.focus({preventScroll: true})); + requestAnimationFrame(() => this.focus(true)); + break; + case "Home": + e.preventDefault(); + rootDataStack.__activateFirst(); + break; + case "End": + e.preventDefault(); + rootDataStack.__activateLast(); + break; + case "Tab": + rootDataStack.__stopTyping(); break; } }); @@ -313,38 +331,62 @@ function createOptionsManager(root) { }, findClosestOption, enableDefaultOptionsHandlers(disabledEvents = []) { - const events = { - click: (optionEl) => { - if (!optionEl.dataset.value) - return; - root.$nextTick(() => root.$refs.__input?.focus({preventScroll: true})); - }, - mouseover: (optionEl) => { - if (!optionEl.dataset.value) + if (!optionsEl) + return; + optionsEl.tabIndex = 0; + if (!disabledEvents.includes("mouseover")) { + this.on("mouseover", (_event, optionEl) => { + if (!optionEl?.dataset.value) return; root.__activate(optionEl.dataset.value); - }, - mousemove: (optionEl) => { - if (!optionEl.dataset.value || root.__isActive(optionEl.dataset.value)) + }); + } + if (!disabledEvents.includes("mousemove")) { + this.on("mousemove", (_event, optionEl) => { + if (!optionEl?.dataset.value) + return; + if (root.__isActive(optionEl.dataset.value)) return; root.__activate(optionEl.dataset.value); - }, - mouseout: () => { + }); + } + if (!disabledEvents.includes("mouseout")) { + this.on("mouseout", () => { if (root.__keepActivated) return; root.__deactivate(); - } - }; - Object.entries(events).forEach(([key, handler]) => { - if (!disabledEvents.includes(key)) { - this.on(key, (event, optionEl) => { - event.stopPropagation(); - if (!optionEl) - return; - handler(optionEl); - }); - } - }); + }); + } + if (!disabledEvents.includes("keydown")) { + this.on("keydown", (event) => { + event.stopPropagation(); + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + root.__activateNext(); + break; + case "ArrowUp": + event.preventDefault(); + root.__activatePrev(); + break; + case "Home": + event.preventDefault(); + root.__activateFirst(); + break; + case "End": + event.preventDefault(); + root.__activateLast(); + break; + case "Escape": + event.preventDefault(); + root.__deactivate(); + break; + case "Tab": + root.__deactivate(); + break; + } + }); + } }, get all() { let allOptions = root.__optionsEls; @@ -402,12 +444,12 @@ function CreateRoverRoot({ __filteredValues: null, __prevVisibleArray: null, __prevActiveValue: void 0, - __effectRAF: NaN, + __effectRAF: null, __inputManager: void 0, __optionsManager: void 0, __optionManager: void 0, __buttonManager: void 0, - __add: (value, disabled) => collection.add(value, disabled), + __add: (value, search, disabled) => collection.add(value, search, disabled), __forget: (value) => collection.forget(value), __activate: (value) => collection.activate(value), __deactivate: () => collection.deactivate(), @@ -425,7 +467,6 @@ function CreateRoverRoot({ this.__isLoading = collection.pending.state; }); this.$watch("_x__searchQuery", (query) => { - this.__isLoading = true; if (query.length > 0) { const results = this.__searchUsingQuery(query).map((r) => r.value); const prev = this.__filteredValues; @@ -441,10 +482,9 @@ function CreateRoverRoot({ if (this.__activatedValue && this.__filteredValues && !this.__filteredValues.includes(this.__activatedValue)) { this.__deactivate(); } - if (this.__isOpen && !this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { + if (!this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { this.__activate(this.__filteredValues[0]); } - this.__isLoading = false; }); this.$nextTick(() => { this.__optionsEls = Array.from(this.$el.querySelectorAll("[x-rover\\:option]")); @@ -458,14 +498,11 @@ function CreateRoverRoot({ const activeItem = this.__getByIndex(collection.activeIndex.value); const activeValue = this.__activatedValue = activeItem?.value; const visibleValuesArray = this.__filteredValues; - if (!Number.isNaN(this.__effectRAF)) - cancelAnimationFrame(this.__effectRAF); - this.__effectRAF = requestAnimationFrame(() => { + requestAnimationFrame(() => { this.__patchItemsVisibility(visibleValuesArray); this.__patchItemsActivity(activeValue); this.__handleSeparatorsVisibility(); this.__handleGroupsVisibility(); - this.__effectRAF = null; }); }); }); @@ -576,8 +613,6 @@ function CreateRoverRoot({ return ++this.__s_id; }, destroy() { - if (this.__effectRAF) - cancelAnimationFrame(this.__effectRAF); this.__inputManager?.destroy(); this.__optionManager?.destroy(); this.__optionsManager?.destroy(); @@ -608,6 +643,14 @@ var rover = (el) => { get isLoading() { return data.__isLoading; }, + get inputEl() { + return data.$root.querySelector("[x-rover\\:input]"); + }, + reindex() { + }, + getOptionElByValue(value) { + return data.__optionIndex?.get(value); + }, activate(key) { data.__collection.activate(key); }, @@ -751,7 +794,6 @@ function rover2(Alpine2) { } function handleInput(Alpine3, el) { Alpine3.bind(el, { - "x-ref": "_x__input", "x-model": "_x__searchQuery", "x-bind:id"() { return this.$id("rover-input"); diff --git a/index.html b/index.html index 467beca..4d85cee 100644 --- a/index.html +++ b/index.html @@ -65,7 +65,7 @@ 'grape' ], selected: 'mango', - count: 2000, + count: 2500, _previousSelected: undefined, diff --git a/src/Managers/InputManager.ts b/src/Managers/InputManager.ts index d18b839..8770684 100644 --- a/src/Managers/InputManager.ts +++ b/src/Managers/InputManager.ts @@ -4,10 +4,14 @@ import { bindListener } from "./utils" export function createInputManager( rootDataStack: RoverRootContext ): InputManager { - const inputEl = rootDataStack.$el.querySelector('[x-rover\\:input]') as HTMLInputElement | undefined; + const inputEl = rootDataStack.$root.querySelector('[x-rover\\:input]') as HTMLInputElement | undefined; - if (!inputEl) { - console.warn(`Input element with [x-rover\\:input] not found`); + const inputElExists = (): boolean => { + if (!inputEl) { + console.warn(`Input element with [x-rover\\:input] not found`); + return false + } + return true } return { @@ -17,13 +21,14 @@ export function createInputManager( eventKey: K, handler: (event: HTMLElementEventMap[K], activeKey: string | undefined) => void ) { - if (!inputEl) return; + if (!inputElExists()) return; + const listener = (event: HTMLElementEventMap[K]) => { handler(event, rootDataStack.__activatedValue ?? undefined); }; - bindListener(inputEl, eventKey, listener, this.controller); + bindListener(inputEl!, eventKey, listener, this.controller); }, get value(): string { @@ -34,8 +39,12 @@ export function createInputManager( if (inputEl) inputEl.value = val; }, + focus(preventScroll: boolean = true): void { + requestAnimationFrame(() => inputEl?.focus({ preventScroll })) + }, + enableDefaultInputHandlers(disabledEvents: Array<'focus' | 'blur' | 'input' | 'keydown'> = []) { - if (!inputEl) return; + if (!inputElExists()) return; if (!disabledEvents.includes('focus')) { this.on('focus', () => rootDataStack.__startTyping()); @@ -60,8 +69,21 @@ export function createInputManager( case 'Escape': e.preventDefault(); e.stopPropagation(); - requestAnimationFrame(() => inputEl?.focus({ preventScroll: true })); + requestAnimationFrame(() => this.focus(true)); + break; + case 'Home': + e.preventDefault(); + rootDataStack.__activateFirst(); break; + + case 'End': + e.preventDefault(); + rootDataStack.__activateLast(); + break; + case 'Tab': + rootDataStack.__stopTyping(); + break; + } }); } diff --git a/src/Managers/OptionManager.ts b/src/Managers/OptionManager.ts index 1221ff4..40a5181 100644 --- a/src/Managers/OptionManager.ts +++ b/src/Managers/OptionManager.ts @@ -31,6 +31,8 @@ export function createOptionManager(root: RoverRootContext): OptionManager { }) }, + + destroy() { this.controller.abort(); } diff --git a/src/Managers/OptionsManager.ts b/src/Managers/OptionsManager.ts index 9136b83..0d31f01 100644 --- a/src/Managers/OptionsManager.ts +++ b/src/Managers/OptionsManager.ts @@ -6,6 +6,7 @@ export function createOptionsManager(root: RoverRootContext): OptionsManager { const optionsEl = root.$el.querySelector('[x-rover\\:options]') as HTMLElement; + if (!optionsEl) console.warn("Options container not found"); const findClosestOption = (el: EventTarget | null): HTMLElement | undefined => { @@ -29,7 +30,7 @@ export function createOptionsManager(root: RoverRootContext): OptionsManager { const listener = (event: HTMLElementEventMap[K]) => { const optionEl = findClosestOption(event.target); - + handler(event, optionEl, root.__activatedValue ?? null); }; @@ -39,37 +40,70 @@ export function createOptionsManager(root: RoverRootContext): OptionsManager { findClosestOption, enableDefaultOptionsHandlers(disabledEvents: string[] = []) { - const events = { - click: (optionEl: HTMLElement) => { - if (!optionEl.dataset.value) return; - root.$nextTick(() => root.$refs.__input?.focus({ preventScroll: true })); - }, - mouseover: (optionEl: HTMLElement) => { - if (!optionEl.dataset.value) return; - root.__activate(optionEl.dataset.value); - }, - mousemove: (optionEl: HTMLElement) => { - if (!optionEl.dataset.value || root.__isActive(optionEl.dataset.value)) return; + if (!optionsEl) return; + + optionsEl.tabIndex = 0; + + + if (!disabledEvents.includes('mouseover')) { + this.on('mouseover', (_event: MouseEvent, optionEl: HTMLElement) => { + if (!optionEl?.dataset.value) return; root.__activate(optionEl.dataset.value); - }, - mouseout: () => { - if (root.__keepActivated) return; - root.__deactivate(); - } - }; + }); + } - Object.entries(events).forEach(([key, handler]) => { - if (!disabledEvents.includes(key)) { - this.on(key, (event: Event, optionEl: HTMLElement) => { - event.stopPropagation(); + if (!disabledEvents.includes('mousemove')) { + this.on('mousemove', (_event: MouseEvent, optionEl: HTMLElement) => { + if (!optionEl?.dataset.value) return; + if (root.__isActive(optionEl.dataset.value)) return; - if (!optionEl) return; + root.__activate(optionEl.dataset.value); + }); + } - handler(optionEl); + if (!disabledEvents.includes('mouseout')) { + this.on('mouseout', () => { + if (root.__keepActivated) return; + root.__deactivate(); + }); + } + + if (!disabledEvents.includes('keydown')) { + this.on('keydown', (event: KeyboardEvent) => { + event.stopPropagation(); + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + root.__activateNext(); + break; + + case 'ArrowUp': + event.preventDefault(); + root.__activatePrev(); + break; + + case 'Home': + event.preventDefault(); + root.__activateFirst(); + break; + + case 'End': + event.preventDefault(); + root.__activateLast(); + break; + + case 'Escape': + event.preventDefault(); + root.__deactivate(); + break; + + case 'Tab': + root.__deactivate(); + break; } - ); - } - }); + }); + } }, diff --git a/src/core/RoverCollection.ts b/src/core/RoverCollection.ts index be0168c..ef23922 100644 --- a/src/core/RoverCollection.ts +++ b/src/core/RoverCollection.ts @@ -30,8 +30,8 @@ export default class RoverCollection { * Collection Management * ------------------------------------- */ - public add(value: string, disabled = false): void { - const item = { value, disabled }; + public add(value: string, searchable: string, disabled = false): void { + const item = { value, disabled, searchable }; this.items.push(item); this.invalidate(); } @@ -108,22 +108,18 @@ export default class RoverCollection { return this.items; } - const normalized = query + const normalizedQuery = query .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, ''); // Incremental search optimization - if (this.currentQuery && normalized.startsWith(this.currentQuery) && this.currentResults.length > 0) { + if (this.currentQuery && normalizedQuery.startsWith(this.currentQuery) && this.currentResults.length > 0) { const filtered = this.currentResults.filter(item => { - const itemNormalized = String(item.value) - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = filtered; this.rebuildNavIndex(); return filtered; @@ -131,14 +127,10 @@ export default class RoverCollection { // Full search - search items directly const results = this.items.filter(item => { - const itemNormalized = String(item.value) - .toLowerCase() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - return itemNormalized.includes(normalized); + return item.searchable.includes(normalizedQuery); }); - this.currentQuery = normalized; + this.currentQuery = normalizedQuery; this.currentResults = results; this.rebuildNavIndex(); diff --git a/src/factories/CreateRoverOption.ts b/src/factories/CreateRoverOption.ts index 6059dd9..5f45f26 100644 --- a/src/factories/CreateRoverOption.ts +++ b/src/factories/CreateRoverOption.ts @@ -8,18 +8,26 @@ export default function CreateRoverOption(Alpine: AlpineType): RoverOptionData { let disabled = Alpine.extractProp(this.$el, 'disabled', false, false) as boolean; let value = Alpine.extractProp(this.$el, 'value', '') as string; - + + const rawSearch = Alpine.extractProp(this.$el, 'data-search', value) as string; + + const normalizedSearch = String(rawSearch).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim(); + this.$el.dataset.value = value; - // Add to collection - this.__add(value, disabled); + // it may fails redudant when the search it self is the value, but anyway + // we need to normalize each item at search time for better i18n support, + // as well as it's highly required feature for adding better search actually + // even when the search query isn't in the label there + this.__add(value, normalizedSearch, disabled); - this.$nextTick((): void => { + this.$nextTick(() => { if (disabled) { this.$el.setAttribute('tabindex', '-1'); } }); - }, + } + , destroy() { this.__forget(this.__uniqueKey); diff --git a/src/factories/CreateRoverRoot.ts b/src/factories/CreateRoverRoot.ts index 424b1b9..c419bf2 100644 --- a/src/factories/CreateRoverRoot.ts +++ b/src/factories/CreateRoverRoot.ts @@ -44,14 +44,14 @@ export default function CreateRoverRoot( __prevVisibleArray: null as string[] | null, __prevActiveValue: undefined, - __effectRAF: NaN, + __effectRAF: null, __inputManager: undefined, __optionsManager: undefined, __optionManager: undefined, __buttonManager: undefined, - __add: (value: string, disabled: boolean) => collection.add(value, disabled), + __add: (value: string, search: string, disabled: boolean) => collection.add(value, search, disabled), __forget: (value: string) => collection.forget(value), __activate: (value: string) => collection.activate(value), __deactivate: () => collection.deactivate(), @@ -72,11 +72,10 @@ export default function CreateRoverRoot( }); this.$watch('_x__searchQuery', (query: string) => { - this.__isLoading = true; - if (query.length > 0) { const results = this.__searchUsingQuery(query).map((r: Item) => r.value); + const prev = this.__filteredValues; const changed = !prev || prev.length !== results.length || results.some((v: unknown, i: number) => v !== prev[i]); @@ -94,17 +93,13 @@ export default function CreateRoverRoot( this.__deactivate(); } - if (this.__isOpen && !this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { + if (!this.__getActiveItem() && this.__filteredValues && this.__filteredValues.length) { this.__activate(this.__filteredValues[0]); } - - this.__isLoading = false; }); this.$nextTick(() => { - this.__optionsEls = Array.from( - this.$el.querySelectorAll('[x-rover\\:option]') - ) as Array; + this.__optionsEls = Array.from(this.$el.querySelectorAll('[x-rover\\:option]')) as Array; this.__optionIndex = new Map(); this.__optionsEls.forEach((el: HTMLElement) => { @@ -114,27 +109,26 @@ export default function CreateRoverRoot( effect(() => { const activeItem = this.__getByIndex(collection.activeIndex.value); + const activeValue = this.__activatedValue = activeItem?.value; const visibleValuesArray = this.__filteredValues; - if (!Number.isNaN(this.__effectRAF)) cancelAnimationFrame(this.__effectRAF); - - this.__effectRAF = requestAnimationFrame(() => { + requestAnimationFrame(() => { this.__patchItemsVisibility(visibleValuesArray); this.__patchItemsActivity(activeValue); this.__handleSeparatorsVisibility(); this.__handleGroupsVisibility(); - this.__effectRAF = null; }); }); }); }, __handleGroupsVisibility() { - + // todo this evenning with vs }, __handleSeparatorsVisibility() { + // todo this evening vith vs }, __patchItemsVisibility(visibleValuesArray: string[] | null) { @@ -189,8 +183,7 @@ export default function CreateRoverRoot( } this.__prevVisibleArray = visibleValuesArray; - } - , + }, __patchItemsActivity(activeValue: string | undefined) { @@ -263,8 +256,6 @@ export default function CreateRoverRoot( }, destroy() { - if (this.__effectRAF) cancelAnimationFrame(this.__effectRAF); - this.__inputManager?.destroy(); this.__optionManager?.destroy(); this.__optionsManager?.destroy(); diff --git a/src/index.ts b/src/index.ts index 4ed8948..262eef3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,7 @@ export default function rover(Alpine: Alpine): void { el: AlpineType.ElementWithXAttributes ): void { Alpine.bind(el, { - 'x-ref': '_x__input', + // 'x-ref': '_x__input', 'x-model': '_x__searchQuery', 'x-bind:id'() { return this.$id('rover-input') }, 'role': 'combobox', diff --git a/src/magics/rover.ts b/src/magics/rover.ts index ceb41af..7f0fbde 100644 --- a/src/magics/rover.ts +++ b/src/magics/rover.ts @@ -24,10 +24,21 @@ export const rover = (el: ElementWithXAttributes) => { return data.__buttonManager; }, + get isLoading(): boolean { return data.__isLoading; }, + get inputEl(): HTMLElement | null { + return data.$root.querySelector('[x-rover\\:input]'); + }, + // re wire up the internal index to catch changes on the dom + reindex(){ + // @todo + }, + getOptionElByValue(value: string): HTMLElement | undefined { + return data.__optionIndex?.get(value); + }, activate(key: string) { data.__collection.activate(key) }, diff --git a/src/types.ts b/src/types.ts index fff87ba..46cef23 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export interface Options { export interface Item { value: string, + searchable: string, disabled: boolean } @@ -44,7 +45,7 @@ export interface RoverRootData extends XDataContext, Record { __buttonManager: ButtonManager | undefined; // Methods - __add: (value: string, disabled: boolean) => void; + __add: (value: string, searchable: string | undefined, disabled: boolean) => void; __forget: (value: string) => void; __activate: (value: string) => void; __isActive: (value: string) => boolean; @@ -114,6 +115,7 @@ export interface InputManager extends Destroyable, Abortable { get value(): string set value(val: string) + focus: (preventScroll: boolean) => void; enableDefaultInputHandlers(disabledEvents: Array<'focus' | 'blur' | 'input' | 'keydown'>): void }