Add supporting themes required for Lotusdocs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import ui from '../src/index.js'
|
||||
|
||||
document.addEventListener('alpine:init', () => {
|
||||
window.Alpine.plugin(ui)
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import ui from '../src/index.js'
|
||||
|
||||
export default ui
|
||||
|
||||
export { ui }
|
||||
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://headlessui.dev/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://headlessui.dev/favicon-16x16.png" />
|
||||
<script src="//cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Examples</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="flex flex-col h-screen overflow-hidden font-sans antialiased text-gray-900 bg-gray-700">
|
||||
<header class="relative z-10 flex items-center justify-between flex-shrink-0 px-4 py-4 bg-gray-700 border-b border-gray-200 sm:px-6 lg:px-8">
|
||||
<a href="/"><svg class="h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 243 42">
|
||||
<path fill="#fff" d="M65.74 13.663c-2.62 0-4.702.958-5.974 2.95V6.499h-4.163V33.32h4.163V23.051c0-3.908 2.159-5.518 4.896-5.518 2.62 0 4.317 1.533 4.317 4.445V33.32h4.162V21.557c0-4.982-3.083-7.894-7.4-7.894zM79.936 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.674-10.116-6.052 0-10.176 4.407-10.176 10.078 0 5.748 4.124 10.078 10.484 10.078 3.778 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.925 1.341-2.66 2.376-4.972 2.376-3.084 0-5.512-1.533-6.168-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.506 0 4.934 1.418 5.512 4.713H79.898zM113.282 14.161v2.72c-1.465-1.992-3.739-3.218-6.746-3.218-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.162V14.16h-4.162zm-6.09 15.71c-3.469 0-6.091-2.567-6.091-6.13 0-3.564 2.622-6.131 6.091-6.131 3.469 0 6.09 2.567 6.09 6.13 0 3.564-2.621 6.132-6.09 6.132zM136.597 6.498v10.384c-1.465-1.993-3.739-3.219-6.746-3.219-5.242 0-9.597 4.368-9.597 10.078 0 5.67 4.355 10.078 9.597 10.078 3.007 0 5.281-1.227 6.746-3.258v2.76h4.163V6.497h-4.163zm-6.09 23.374c-3.469 0-6.09-2.568-6.09-6.131 0-3.564 2.621-6.131 6.09-6.131s6.09 2.567 6.09 6.13c0 3.564-2.621 6.132-6.09 6.132zM144.648 33.32h4.163V5.348h-4.163V33.32zM155.957 25.503h15.341c.077-.536.154-1.15.154-1.724 0-5.518-3.931-10.116-9.675-10.116-6.051 0-10.176 4.407-10.176 10.078 0 5.748 4.125 10.078 10.485 10.078 3.777 0 6.668-1.572 8.441-4.177l-3.43-1.993c-.926 1.341-2.66 2.376-4.973 2.376-3.083 0-5.512-1.533-6.167-4.521zm-.038-3.372c.578-2.873 2.698-4.713 5.82-4.713 2.505 0 4.934 1.418 5.512 4.713h-11.332zM177.137 19.45c0-1.38 1.311-2.032 2.814-2.032 1.581 0 2.93.69 3.623 2.184l3.508-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.279-2.759l-3.584 2.07c1.233 2.758 4.008 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.369-4.98-10.369-8.468zM192.774 19.45c0-1.38 1.31-2.032 2.813-2.032 1.581 0 2.93.69 3.624 2.184l3.507-1.954c-1.349-2.529-3.97-3.985-7.131-3.985-3.931 0-7.053 2.26-7.053 5.863 0 6.859 10.368 4.943 10.368 8.353 0 1.533-1.426 2.146-3.276 2.146-2.12 0-3.662-1.035-4.278-2.759l-3.585 2.07c1.233 2.758 4.009 4.483 7.863 4.483 4.163 0 7.516-2.07 7.516-5.902 0-7.088-10.368-4.98-10.368-8.468zM224.523 28.9c2.889 0 5.027-1.715 5.027-4.53v-8.782h-2.588v8.577c0 1.268-.676 2.219-2.439 2.219s-2.438-.951-2.438-2.22v-8.576h-2.569v8.782c0 2.815 2.138 4.53 5.007 4.53zM232.257 15.588V28.64h2.588V15.588h-2.588z"> </path>
|
||||
<path fill="#fff" fill-rule="evenodd" d="M233.817 9.328H220.42c-2.96 0-5.359 2.385-5.359 5.327v13.318c0 2.942 2.399 5.327 5.359 5.327h13.397c2.959 0 5.358-2.385 5.358-5.327V14.655c0-2.942-2.399-5.327-5.358-5.327zM220.42 6.664c-4.439 0-8.038 3.578-8.038 7.99v13.319c0 4.413 3.599 7.99 8.038 7.99h13.397c4.439 0 8.038-3.577 8.038-7.99V14.655c0-4.413-3.599-7.99-8.038-7.99H220.42z" clip-rule="evenodd"></path>
|
||||
<path fill="#fff" fill-rule="evenodd" d="M220.42 9.328h13.397c2.959 0 5.358 2.385 5.358 5.327v13.318c0 2.942-2.399 5.327-5.358 5.327H220.42c-2.96 0-5.359-2.385-5.359-5.327V14.655c0-2.942 2.399-5.327 5.359-5.327zm-8.038 5.327c0-4.413 3.599-7.99 8.038-7.99h13.397c4.439 0 8.038 3.577 8.038 7.99v13.318c0 4.413-3.599 7.99-8.038 7.99H220.42c-4.439 0-8.038-3.577-8.038-7.99V14.655z" clip-rule="evenodd"></path>
|
||||
<path fill="url(#prefix__paint0_linear)" d="M8.577 26.097l25.779-8.556c-.514-3.201-.88-5.342-1.307-6.974-.457-1.756-.821-2.226-.965-2.39a5.026 5.026 0 00-1.81-1.306c-.2-.086-.762-.284-2.583-.175-1.924.116-4.453.507-8.455 1.137-4.003.63-6.529 1.035-8.395 1.516-1.766.456-2.239.817-2.403.96a4.999 4.999 0 00-1.315 1.8c-.085.198-.285.757-.175 2.568.116 1.913.51 4.426 1.143 8.405.178 1.114.337 2.113.486 3.015z"> </path>
|
||||
<path fill="url(#prefix__paint1_linear)" fill-rule="evenodd" d="M1.47 24.124C.244 16.427-.37 12.58.96 9.49A11.665 11.665 0 014.027 5.29c2.545-2.21 6.416-2.82 14.16-4.039C25.93.031 29.8-.578 32.907.743a11.729 11.729 0 014.225 3.05c2.223 2.53 2.836 6.38 4.063 14.076 1.226 7.698 1.84 11.546.511 14.636a11.666 11.666 0 01-3.069 4.199c-2.545 2.21-6.416 2.82-14.159 4.039-7.743 1.219-11.614 1.828-14.722.508a11.728 11.728 0 01-4.224-3.05C3.31 35.67 2.697 31.82 1.47 24.123zm13.657 13.668c2.074-.125 4.743-.54 8.697-1.163 3.953-.622 6.62-1.047 8.632-1.566 1.949-.502 2.846-.992 3.426-1.496a7.5 7.5 0 001.973-2.7c.302-.703.494-1.703.372-3.7-.125-2.063-.543-4.716-1.17-8.646-.625-3.93-1.053-6.582-1.574-8.582-.506-1.937-.999-2.83-1.505-3.405a7.54 7.54 0 00-2.716-1.961c-.707-.301-1.713-.492-3.723-.371-2.074.125-4.743.54-8.697 1.163-3.953.622-6.62 1.047-8.632 1.565-1.949.503-2.846.993-3.426 1.497a7.5 7.5 0 00-1.972 2.699c-.303.704-.495 1.704-.373 3.701.125 2.062.543 4.716 1.17 8.646.625 3.93 1.053 6.582 1.574 8.581.506 1.938 1 2.83 1.505 3.406a7.54 7.54 0 002.716 1.961c.707.3 1.713.492 3.723.37z" clip-rule="evenodd"></path>
|
||||
<defs>
|
||||
<linearGradient id="prefix__paint0_linear" x1="16.759" x2="23.386" y1="0" y2="41.662"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#66E3FF"></stop>
|
||||
<stop offset="1" stop-color="#7064F9"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="prefix__paint1_linear" x1="16.759" x2="23.386" y1="0" y2="41.662"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#66E3FF"></stop>
|
||||
<stop offset="1" stop-color="#7064F9"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg></a></header>
|
||||
<main class="flex-1 overflow-auto bg-gray-50">
|
||||
<div class="container my-24">
|
||||
<div class="prose">
|
||||
<h2>Examples</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">dialog</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/dialog/dialog.html">dialog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">disclosure</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/disclosure/disclosure.html">disclosure</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">listbox</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="demo/listbox">listbox with
|
||||
pure tailwind</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">menu</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/menu/menu.html">menu</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">popover</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/popover/popover.html">popover</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">radio group</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/radio-group/radio-group.html">radio group</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">switch</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/switch/switch.html">switch with pure
|
||||
tailwind</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h3 class="text-xl capitalize">tabs</h3>
|
||||
<ul>
|
||||
<li><a class="capitalize" href="/tabs/tabs.html">tabs with pure
|
||||
tailwind</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://headlessui.dev/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://headlessui.dev/favicon-16x16.png" />
|
||||
|
||||
<script src="/packages/intersect/dist/cdn.js" defer></script>
|
||||
<script src="/packages/morph/dist/cdn.js" defer></script>
|
||||
<script src="/packages/persist/dist/cdn.js"></script>
|
||||
<script src="/packages/focus/dist/cdn.js"></script>
|
||||
<script src="/packages/mask/dist/cdn.js"></script>
|
||||
<script src="/packages/ui/dist/cdn.js" defer></script>
|
||||
<script src="/packages/alpinejs/dist/cdn.js" defer></script>
|
||||
<script src="//cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Listbox</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="flex flex-col h-screen overflow-hidden font-sans antialiased text-gray-900 bg-gray-700">
|
||||
<div
|
||||
x-data="{ selected: undefined, people: [
|
||||
{ id: 1, name: 'Wade Cooper' },
|
||||
{ id: 2, name: 'Arlene Mccoy' },
|
||||
{ id: 3, name: 'Devon Webb' },
|
||||
{ id: 4, name: 'Tom Cook' },
|
||||
{ id: 5, name: 'Tanya Fox', disabled: true },
|
||||
{ id: 6, name: 'Hellen Schmidt' },
|
||||
{ id: 7, name: 'Caroline Schultz' },
|
||||
{ id: 8, name: 'Mason Heaney' },
|
||||
{ id: 9, name: 'Claudie Smitham' },
|
||||
{ id: 10, name: 'Emil Schaefer' },
|
||||
]}"
|
||||
class="flex justify-center w-screen h-full p-12 bg-gray-50"
|
||||
>
|
||||
<div class="w-full max-w-xs mx-auto">
|
||||
<div class="flex justify-between mb-8">
|
||||
<button class="underline" @click="selected = people[1]">Change value</button>
|
||||
|
||||
<button class="underline" @click="
|
||||
people.sort((a, b) => a.name > b.name ? 1 : -1)
|
||||
">Reorder</button>
|
||||
|
||||
<button class="underline" @click="
|
||||
people = people.filter(i => i.name !== 'Arlene Mccoy')
|
||||
">Destroy item</button>
|
||||
</div>
|
||||
|
||||
<div x-listbox name="something" x-model="selected" class="space-y-1">
|
||||
<label x-listbox:label class="block text-sm font-medium leading-5 text-gray-700 mb-1">
|
||||
Assigned to
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button x-listbox:button class="relative w-full py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5">
|
||||
<span class="block truncate" x-text="selected ? selected.name : 'Select Person'"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="w-5 h-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div class="absolute w-full mt-1 bg-white rounded-md shadow-lg">
|
||||
<ul x-listbox:options class="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5">
|
||||
<template x-for="person in people" :key="person.id">
|
||||
<li
|
||||
x-listbox:option :value="person"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:disabled="person.disabled"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900', person.disabled && 'bg-gray-50 text-gray-300'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'" x-text="person.name"></span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://headlessui.dev/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://headlessui.dev/favicon-16x16.png" />
|
||||
|
||||
<script src="/packages/intersect/dist/cdn.js" defer></script>
|
||||
<script src="/packages/morph/dist/cdn.js" defer></script>
|
||||
<script src="/packages/persist/dist/cdn.js"></script>
|
||||
<script src="/packages/focus/dist/cdn.js"></script>
|
||||
<script src="/packages/mask/dist/cdn.js"></script>
|
||||
<script src="/packages/ui/dist/cdn.js" defer></script>
|
||||
<script src="/packages/alpinejs/dist/cdn.js" defer></script>
|
||||
<script src="//cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Listbox</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="flex flex-col h-screen overflow-hidden font-sans antialiased text-gray-900 bg-gray-700">
|
||||
<div x-data="{ selected: 'Wade Cooper' }" class="flex justify-center w-screen h-full p-12 bg-gray-50">
|
||||
<div class="w-full max-w-xs mx-auto">
|
||||
<div class="flex justify-between mb-8">
|
||||
<button class="underline" @click="selected = Arlene Mccoy">Change value</button>
|
||||
</div>
|
||||
|
||||
<div x-listbox name="something" x-model="selected" class="space-y-1">
|
||||
<label x-listbox:label class="block text-sm font-medium leading-5 text-gray-700 mb-1">
|
||||
Assigned to
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button x-listbox:button class="relative w-full py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5">
|
||||
<span class="block truncate" x-text="selected"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="w-5 h-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div class="absolute w-full mt-1 bg-white rounded-md shadow-lg">
|
||||
<ul x-listbox:options class="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5">
|
||||
<li
|
||||
x-listbox:option value="Wade Cooper"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Wade Cooper</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Arlene Mccoy"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Arlene Mccoy</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Devon Webb"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Devon Webb</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Tom Cook"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Tom Cook</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Tanya Fox"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Tanya Fox</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Hellen Schmidt"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Hellen Schmidt</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Caroline Schultz"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Caroline Schultz</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Mason Heaney"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Mason Heaney</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Claudie Smitham"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Claudie Smitham</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
x-listbox:option value="Emil Schaefer"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'">Emil Schaefer</span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="https://headlessui.dev/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="https://headlessui.dev/favicon-16x16.png" />
|
||||
|
||||
<script src="/packages/intersect/dist/cdn.js" defer></script>
|
||||
<script src="/packages/morph/dist/cdn.js" defer></script>
|
||||
<script src="/packages/persist/dist/cdn.js"></script>
|
||||
<script src="/packages/focus/dist/cdn.js"></script>
|
||||
<script src="/packages/mask/dist/cdn.js"></script>
|
||||
<script src="/packages/ui/dist/cdn.js" defer></script>
|
||||
<script src="/packages/alpinejs/dist/cdn.js" defer></script>
|
||||
<script src="//cdn.tailwindcss.com"></script>
|
||||
|
||||
<title>Listbox</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="flex flex-col h-screen overflow-hidden font-sans antialiased text-gray-900 bg-gray-700">
|
||||
<div
|
||||
x-data="{ selected: [], people: [
|
||||
{ id: 1, name: 'Wade Cooper' },
|
||||
{ id: 2, name: 'Arlene Mccoy' },
|
||||
{ id: 3, name: 'Devon Webb' },
|
||||
{ id: 4, name: 'Tom Cook' },
|
||||
{ id: 5, name: 'Tanya Fox', disabled: true },
|
||||
{ id: 6, name: 'Hellen Schmidt' },
|
||||
{ id: 7, name: 'Caroline Schultz' },
|
||||
{ id: 8, name: 'Mason Heaney' },
|
||||
{ id: 9, name: 'Claudie Smitham' },
|
||||
{ id: 10, name: 'Emil Schaefer' },
|
||||
]}"
|
||||
class="flex justify-center w-screen h-full p-12 bg-gray-50"
|
||||
>
|
||||
<div class="w-full max-w-xs mx-auto">
|
||||
<div class="flex justify-between mb-8">
|
||||
<button class="underline" @click="selected.push(people[1])">Change value</button>
|
||||
|
||||
<button class="underline" @click="
|
||||
people.sort((a, b) => a.name > b.name ? 1 : -1)
|
||||
">Reorder</button>
|
||||
|
||||
<button class="underline" @click="
|
||||
people = people.filter(i => i.name !== 'Arlene Mccoy')
|
||||
">Destroy item</button>
|
||||
</div>
|
||||
|
||||
<div x-listbox name="people" x-model="selected" multiple class="space-y-1">
|
||||
<label x-listbox:label class="block text-sm font-medium leading-5 text-gray-700 mb-1">
|
||||
Assigned to
|
||||
</label>
|
||||
|
||||
<div class="relative">
|
||||
<span class="inline-block w-full rounded-md shadow-sm">
|
||||
<button x-listbox:button class="relative w-full py-2 pl-3 pr-10 text-left transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md cursor-default focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm sm:leading-5">
|
||||
<span class="block truncate" x-text="selected.length > 0 ? selected.map(i => i.name).join(', ') : 'Select Person'"></span>
|
||||
<span class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="w-5 h-5 text-gray-400" viewBox="0 0 20 20" fill="none" stroke="currentColor">
|
||||
<path d="M7 7l3-3 3 3m0 6l-3 3-3-3" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div class="absolute w-full mt-1 bg-white rounded-md shadow-lg">
|
||||
<ul x-listbox:options class="py-1 overflow-auto text-base leading-6 rounded-md shadow-xs max-h-60 focus:outline-none sm:text-sm sm:leading-5">
|
||||
<template x-for="person in people" :key="person.id">
|
||||
<li
|
||||
x-listbox:option :value="person"
|
||||
class="relative py-2 pl-3 cursor-default select-none pr-9 focus:outline-none"
|
||||
:disabled="person.disabled"
|
||||
:class="[$listboxOption.isActive ? 'text-white bg-indigo-600' : 'text-gray-900', person.disabled && 'bg-gray-50 text-gray-300'].filter(Boolean)"
|
||||
>
|
||||
<span class="block truncate" :class="$listboxOption.isSelected ? 'font-semibold' : 'font-normal'" x-text="person.name"></span>
|
||||
|
||||
<span
|
||||
x-show="$listboxOption.isSelected"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
:class="$listboxOption.isActive ? 'text-white' : 'text-indigo-600'"
|
||||
>
|
||||
<svg class="w-5 h-5" viewbox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
|
||||
2492
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/cdn.js
vendored
Normal file
2492
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/cdn.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/cdn.min.js
vendored
Normal file
1
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/cdn.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2520
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/module.cjs.js
vendored
Normal file
2520
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/module.cjs.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2492
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/module.esm.js
vendored
Normal file
2492
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/dist/module.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@alpinejs/ui",
|
||||
"version": "3.13.7-beta.0",
|
||||
"description": "Headless UI components for Alpine",
|
||||
"homepage": "https://alpinejs.dev/components#headless",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/alpinejs/alpine.git",
|
||||
"directory": "packages/ui"
|
||||
},
|
||||
"author": "Caleb Porzio",
|
||||
"license": "MIT",
|
||||
"main": "dist/module.cjs.js",
|
||||
"module": "dist/module.esm.js",
|
||||
"unpkg": "dist/cdn.min.js",
|
||||
"devDependencies": {}
|
||||
}
|
||||
510
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/combobox.js
Normal file
510
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/combobox.js
Normal file
@@ -0,0 +1,510 @@
|
||||
import { generateContext, renderHiddenInputs } from './list-context'
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('combobox', (el, directive, { evaluate }) => {
|
||||
if (directive.value === 'input') handleInput(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'options') handleOptions(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine, directive, evaluate)
|
||||
else handleRoot(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('combobox', el => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get value() {
|
||||
return data.__value
|
||||
},
|
||||
get isOpen() {
|
||||
return data.__isOpen
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__isDisabled
|
||||
},
|
||||
get activeOption() {
|
||||
let active = data.__context?.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get activeIndex() {
|
||||
let active = data.__context?.getActiveItem()
|
||||
|
||||
if (active) {
|
||||
return Object.values(Alpine.raw(data.__context.items)).findIndex(i => Alpine.raw(active) == Alpine.raw(i))
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('comboboxOption', el => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
// It's not great depending on the existance of the attribute in the DOM
|
||||
// but it's probably the fastest and most reliable at this point...
|
||||
let optionEl = Alpine.findClosest(el, i => {
|
||||
return i.hasAttribute('x-combobox:option')
|
||||
})
|
||||
|
||||
if (! optionEl) throw 'No x-combobox:option directive found...'
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
get isSelected() {
|
||||
return data.__isSelected(optionEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-combobox-button', 'alpine-combobox-options', 'alpine-combobox-label'] },
|
||||
'x-modelable': '__value',
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
/**
|
||||
* Combobox state...
|
||||
*/
|
||||
__ready: false,
|
||||
__value: null,
|
||||
__isOpen: false,
|
||||
__context: undefined,
|
||||
__isMultiple: undefined,
|
||||
__isStatic: false,
|
||||
__isDisabled: undefined,
|
||||
__displayValue: undefined,
|
||||
__compareBy: null,
|
||||
__inputName: null,
|
||||
__isTyping: false,
|
||||
__hold: false,
|
||||
|
||||
/**
|
||||
* Combobox initialization...
|
||||
*/
|
||||
init() {
|
||||
this.__isMultiple = Alpine.extractProp(el, 'multiple', false)
|
||||
this.__isDisabled = Alpine.extractProp(el, 'disabled', false)
|
||||
this.__inputName = Alpine.extractProp(el, 'name', null)
|
||||
this.__nullable = Alpine.extractProp(el, 'nullable', false)
|
||||
this.__compareBy = Alpine.extractProp(el, 'by')
|
||||
|
||||
this.__context = generateContext(Alpine, this.__isMultiple, 'vertical', () => this.__activateSelectedOrFirst())
|
||||
|
||||
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
|
||||
|
||||
this.__value = defaultValue
|
||||
|
||||
// We have to wait again until after the "ready" processes are finished
|
||||
// to settle up currently selected Values (this prevents this next bit
|
||||
// of code from running multiple times on startup...)
|
||||
queueMicrotask(() => {
|
||||
Alpine.effect(() => {
|
||||
// Everytime the value changes, we need to re-render the hidden inputs,
|
||||
// if a user passed the "name" prop...
|
||||
this.__inputName && renderHiddenInputs(Alpine, this.$el, this.__inputName, this.__value)
|
||||
})
|
||||
|
||||
// Set initial combobox values in the input and properly clear it when the value is reset programmatically...
|
||||
Alpine.effect(() => ! this.__isMultiple && this.__resetInput())
|
||||
})
|
||||
},
|
||||
__startTyping() {
|
||||
this.__isTyping = true
|
||||
},
|
||||
__stopTyping() {
|
||||
this.__isTyping = false
|
||||
},
|
||||
__resetInput() {
|
||||
let input = this.$refs.__input
|
||||
|
||||
if (! input) return
|
||||
|
||||
let value = this.__getCurrentValue()
|
||||
|
||||
input.value = value
|
||||
},
|
||||
__getCurrentValue() {
|
||||
if (! this.$refs.__input) return ''
|
||||
if (! this.__value) return ''
|
||||
if (this.__displayValue) return this.__displayValue(this.__value)
|
||||
if (typeof this.__value === 'string') return this.__value
|
||||
return ''
|
||||
},
|
||||
__open() {
|
||||
if (this.__isOpen) return
|
||||
this.__isOpen = true
|
||||
|
||||
let input = this.$refs.__input
|
||||
|
||||
// Make sure we always notify the parent component
|
||||
// that the starting value is the empty string
|
||||
// when we open the combobox (ignoring any existing value)
|
||||
// to avoid inconsistent displaying.
|
||||
// Setting the input to empty and back to the real value
|
||||
// also helps VoiceOver to annunce the content properly
|
||||
// See https://github.com/tailwindlabs/headlessui/pull/2153
|
||||
if (input) {
|
||||
let value = input.value
|
||||
let { selectionStart, selectionEnd, selectionDirection } = input
|
||||
input.value = ''
|
||||
input.dispatchEvent(new Event('change'))
|
||||
input.value = value
|
||||
if (selectionDirection !== null) {
|
||||
input.setSelectionRange(selectionStart, selectionEnd, selectionDirection)
|
||||
} else {
|
||||
input.setSelectionRange(selectionStart, selectionEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => {
|
||||
this.$refs.__input.focus({ preventScroll: true })
|
||||
this.__activateSelectedOrFirst()
|
||||
})
|
||||
},
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
|
||||
this.__context.deactivate()
|
||||
},
|
||||
__activateSelectedOrFirst(activateSelected = true) {
|
||||
if (! this.__isOpen) return
|
||||
|
||||
if (this.__context.hasActive() && this.__context.wasActivatedByKeyPress()) return
|
||||
|
||||
let firstSelectedValue
|
||||
|
||||
if (this.__isMultiple) {
|
||||
let selectedItem = this.__context.getItemsByValues(this.__value)
|
||||
|
||||
firstSelectedValue = selectedItem.length ? selectedItem[0].value : null
|
||||
} else {
|
||||
firstSelectedValue = this.__value
|
||||
}
|
||||
|
||||
let firstSelected = null
|
||||
if (activateSelected && firstSelectedValue) {
|
||||
firstSelected = this.__context.getItemByValue(firstSelectedValue)
|
||||
}
|
||||
|
||||
if (firstSelected) {
|
||||
this.__context.activateAndScrollToKey(firstSelected.key)
|
||||
return
|
||||
}
|
||||
|
||||
this.__context.activateAndScrollToKey(this.__context.firstKey())
|
||||
},
|
||||
__selectActive() {
|
||||
let active = this.__context.getActiveItem()
|
||||
if (active) this.__toggleSelected(active.value)
|
||||
},
|
||||
__selectOption(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (item) this.__toggleSelected(item.value)
|
||||
},
|
||||
__isSelected(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (! item) return false
|
||||
if (item.value === null || item.value === undefined) return false
|
||||
|
||||
return this.__hasSelected(item.value)
|
||||
},
|
||||
__toggleSelected(value) {
|
||||
if (! this.__isMultiple) {
|
||||
this.__value = value
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let index = this.__value.findIndex(j => this.__compare(j, value))
|
||||
|
||||
if (index === -1) {
|
||||
this.__value.push(value)
|
||||
} else {
|
||||
this.__value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
__hasSelected(value) {
|
||||
if (! this.__isMultiple) return this.__compare(this.__value, value)
|
||||
|
||||
return this.__value.some(i => this.__compare(i, value))
|
||||
},
|
||||
__compare(a, b) {
|
||||
let by = this.__compareBy
|
||||
|
||||
if (! by) by = (a, b) => Alpine.raw(a) === Alpine.raw(b)
|
||||
|
||||
if (typeof by === 'string') {
|
||||
let property = by
|
||||
by = (a, b) => {
|
||||
// Handle null values
|
||||
if ((! a || typeof a !== 'object') || (! b || typeof b !== 'object')) {
|
||||
return Alpine.raw(a) === Alpine.raw(b)
|
||||
}
|
||||
|
||||
|
||||
return a[property] === b[property];
|
||||
}
|
||||
}
|
||||
|
||||
return by(a, b)
|
||||
},
|
||||
}
|
||||
},
|
||||
// Register event listeners..
|
||||
'@mousedown.window'(e) {
|
||||
if (
|
||||
!! ! this.$refs.__input.contains(e.target)
|
||||
&& ! this.$refs.__button.contains(e.target)
|
||||
&& ! this.$refs.__options.contains(e.target)
|
||||
) {
|
||||
this.__close()
|
||||
this.__resetInput()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleInput(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__input',
|
||||
':id'() { return this.$id('alpine-combobox-input') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'combobox',
|
||||
'tabindex': '0',
|
||||
'aria-autocomplete': 'list',
|
||||
|
||||
// We need to defer this evaluation a bit because $refs that get declared later
|
||||
// in the DOM aren't available yet when x-ref is the result of an Alpine.bind object.
|
||||
async ':aria-controls'() { return await microtask(() => this.$refs.__options && this.$refs.__options.id) },
|
||||
':aria-expanded'() { return this.$data.__isDisabled ? undefined : this.$data.__isOpen },
|
||||
':aria-multiselectable'() { return this.$data.__isMultiple ? true : undefined },
|
||||
':aria-activedescendant'() {
|
||||
if (! this.$data.__context.hasActive()) return
|
||||
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
|
||||
return active ? active.el.id : null
|
||||
},
|
||||
':aria-labelledby'() { return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null) },
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
let displayValueFn = Alpine.extractProp(this.$el, 'display-value')
|
||||
if (displayValueFn) this.$data.__displayValue = displayValueFn
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@input.stop'(e) {
|
||||
if(this.$data.__isTyping) {
|
||||
this.$data.__open();
|
||||
this.$dispatch('change')
|
||||
}
|
||||
},
|
||||
'@blur'() { this.$data.__stopTyping(false) },
|
||||
'@keydown'(e) {
|
||||
queueMicrotask(() => this.$data.__context.activateByKeyEvent(e, false, () => this.$data.__isOpen, () => this.$data.__open(), (state) => this.$data.__isTyping = state))
|
||||
},
|
||||
'@keydown.enter.prevent.stop'() {
|
||||
this.$data.__selectActive()
|
||||
|
||||
this.$data.__stopTyping()
|
||||
|
||||
if (! this.$data.__isMultiple) {
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
}
|
||||
},
|
||||
'@keydown.escape.prevent'(e) {
|
||||
if (! this.$data.__static) e.stopPropagation()
|
||||
|
||||
this.$data.__stopTyping()
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
|
||||
},
|
||||
'@keydown.tab'() {
|
||||
this.$data.__stopTyping()
|
||||
if (this.$data.__isOpen) { this.$data.__close() }
|
||||
this.$data.__resetInput()
|
||||
},
|
||||
'@keydown.backspace'(e) {
|
||||
if (this.$data.__isMultiple) return
|
||||
if (! this.$data.__nullable) return
|
||||
|
||||
let input = e.target
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (input.value === '') {
|
||||
this.$data.__value = null
|
||||
|
||||
let options = this.$refs.__options
|
||||
if (options) {
|
||||
options.scrollTop = 0
|
||||
}
|
||||
|
||||
this.$data.__context.deactivate()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__button',
|
||||
':id'() { return this.$id('alpine-combobox-button') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'aria-haspopup': 'true',
|
||||
// We need to defer this evaluation a bit because $refs that get declared later
|
||||
// in the DOM aren't available yet when x-ref is the result of an Alpine.bind object.
|
||||
async ':aria-controls'() { return await microtask(() => this.$refs.__options && this.$refs.__options.id) },
|
||||
':aria-labelledby'() { return this.$refs.__label ? [this.$refs.__label.id, this.$el.id].join(' ') : null },
|
||||
':aria-expanded'() { return this.$data.__isDisabled ? null : this.$data.__isOpen },
|
||||
':disabled'() { return this.$data.__isDisabled },
|
||||
'tabindex': '-1',
|
||||
|
||||
// Initialize....
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && ! this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
|
||||
// Register listeners...
|
||||
'@click'(e) {
|
||||
if (this.$data.__isDisabled) return
|
||||
if (this.$data.__isOpen) {
|
||||
this.$data.__close()
|
||||
this.$data.__resetInput()
|
||||
} else {
|
||||
e.preventDefault()
|
||||
this.$data.__open()
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__label',
|
||||
':id'() { return this.$id('alpine-combobox-label') },
|
||||
'@click'() { this.$refs.__input.focus({ preventScroll: true }) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOptions(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__options',
|
||||
':id'() { return this.$id('alpine-combobox-options') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'listbox',
|
||||
':aria-labelledby'() { return this.$refs.__label ? this.$refs.__label.id : (this.$refs.__button ? this.$refs.__button.id : null) },
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.bound(this.$el, 'static', false)
|
||||
|
||||
if (Alpine.bound(this.$el, 'hold')) {
|
||||
this.$data.__hold = true;
|
||||
}
|
||||
},
|
||||
|
||||
'x-show'() { return this.$data.__isStatic ? true : this.$data.__isOpen },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-combobox-option'] },
|
||||
':id'() { return this.$id('alpine-combobox-option') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'option',
|
||||
':tabindex'() { return this.$comboboxOption.isDisabled ? undefined : '-1' },
|
||||
|
||||
// Only the active element should have aria-selected="true"...
|
||||
'x-effect'() {
|
||||
this.$comboboxOption.isSelected
|
||||
? el.setAttribute('aria-selected', true)
|
||||
: el.setAttribute('aria-selected', false)
|
||||
},
|
||||
|
||||
':aria-disabled'() { return this.$comboboxOption.isDisabled },
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
'__optionKey': null,
|
||||
|
||||
init() {
|
||||
this.__optionKey = (Math.random() + 1).toString(36).substring(7)
|
||||
|
||||
let value = Alpine.extractProp(this.$el, 'value')
|
||||
let disabled = Alpine.extractProp(this.$el, 'disabled', false, false)
|
||||
|
||||
// memoize the context as it's not going to change
|
||||
// and calling this.$data on mouse action is expensive
|
||||
this.__context.registerItem(this.__optionKey, this.$el, value, disabled)
|
||||
},
|
||||
destroy() {
|
||||
this.__context.unregisterItem(this.__optionKey)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@click'() {
|
||||
if (this.$comboboxOption.isDisabled) return;
|
||||
|
||||
this.__selectOption(this.$el)
|
||||
|
||||
if (! this.__isMultiple) {
|
||||
this.__close()
|
||||
this.__resetInput()
|
||||
}
|
||||
|
||||
this.$nextTick(() => this.$refs.__input.focus({ preventScroll: true }))
|
||||
},
|
||||
'@mouseenter'(e) {
|
||||
this.__context.activateEl(this.$el)
|
||||
},
|
||||
'@mousemove'(e) {
|
||||
if (this.__context.isActiveEl(this.$el)) return
|
||||
|
||||
this.__context.activateEl(this.$el)
|
||||
},
|
||||
'@mouseleave'(e) {
|
||||
if (this.__hold) return
|
||||
|
||||
this.__context.deactivate()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Little utility to defer a callback into the microtask queue...
|
||||
function microtask(callback) {
|
||||
return new Promise(resolve => queueMicrotask(() => resolve(callback())))
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('dialog', (el, directive) => {
|
||||
if (directive.value === 'overlay') handleOverlay(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'title') handleTitle(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
else handleRoot(el, Alpine)
|
||||
})
|
||||
|
||||
Alpine.magic('dialog', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
// Kept here for legacy. Remove after out of beta.
|
||||
get open() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
get isOpen() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
// If the user chose to use :open and @close instead of x-model.
|
||||
(Alpine.bound(el, 'open') !== undefined) && Alpine.effect(() => {
|
||||
this.__isOpenState = Alpine.bound(el, 'open')
|
||||
})
|
||||
|
||||
if (Alpine.bound(el, 'initial-focus') !== undefined) this.$watch('__isOpenState', () => {
|
||||
if (! this.__isOpenState) return
|
||||
|
||||
setTimeout(() => {
|
||||
Alpine.bound(el, 'initial-focus').focus()
|
||||
}, 0);
|
||||
})
|
||||
},
|
||||
__isOpenState: false,
|
||||
__close() {
|
||||
if (Alpine.bound(el, 'open')) this.$dispatch('close')
|
||||
else this.__isOpenState = false
|
||||
},
|
||||
get __isOpen() {
|
||||
return Alpine.bound(el, 'static', this.__isOpenState)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-modelable': '__isOpenState',
|
||||
'x-id'() { return ['alpine-dialog-title', 'alpine-dialog-description'] },
|
||||
'x-show'() { return this.__isOpen },
|
||||
'x-trap.inert.noscroll'() { return this.__isOpen },
|
||||
'@keydown.escape'() { this.__close() },
|
||||
':aria-labelledby'() { return this.$id('alpine-dialog-title') },
|
||||
':aria-describedby'() { return this.$id('alpine-dialog-description') },
|
||||
'role': 'dialog',
|
||||
'aria-modal': 'true',
|
||||
})
|
||||
}
|
||||
|
||||
function handleOverlay(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$data.__isOpen === undefined) console.warn('"x-dialog:overlay" is missing a parent element with "x-dialog".') },
|
||||
'x-show'() { return this.__isOpen },
|
||||
'@click.prevent.stop'() { this.$data.__close() },
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
})
|
||||
}
|
||||
|
||||
function handleTitle(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$data.__isOpen === undefined) console.warn('"x-dialog:title" is missing a parent element with "x-dialog".') },
|
||||
':id'() { return this.$id('alpine-dialog-title') },
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
':id'() { return this.$id('alpine-dialog-description') },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('disclosure', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('disclosure', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isOpen() {
|
||||
return $data.__isOpen
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__isOpen',
|
||||
'x-data'() {
|
||||
return {
|
||||
// The panel will call this...
|
||||
// We can't do this inside a microtask in x-init because, when default-open is set to "true",
|
||||
// It will cause the panel to transition in for the first time, instead of showing instantly...
|
||||
__determineDefaultOpenState() {
|
||||
let defaultIsOpen = Boolean(Alpine.bound(this.$el, 'default-open', false))
|
||||
|
||||
if (defaultIsOpen) this.__isOpen = defaultIsOpen
|
||||
},
|
||||
__isOpen: false,
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
},
|
||||
__toggle() {
|
||||
this.__isOpen = ! this.__isOpen
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-disclosure-panel'] },
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
},
|
||||
'@click'() {
|
||||
this.$data.__isOpen = ! this.$data.__isOpen
|
||||
},
|
||||
':aria-expanded'() {
|
||||
return this.$data.__isOpen
|
||||
},
|
||||
':aria-controls'() {
|
||||
return this.$data.$id('alpine-disclosure-panel')
|
||||
},
|
||||
'@keydown.space.prevent.stop'() { this.$data.__toggle() },
|
||||
'@keydown.enter.prevent.stop'() { this.$data.__toggle() },
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
// the Space key doesn't cancel the handleKeyUp, which in turn
|
||||
// triggers a *click*.
|
||||
'@keyup.space.prevent'() {},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
this.$data.__determineDefaultOpenState()
|
||||
},
|
||||
'x-show'() {
|
||||
return this.$data.__isOpen
|
||||
},
|
||||
':id'() {
|
||||
return this.$data.$id('alpine-disclosure-panel')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import combobox from './combobox'
|
||||
import dialog from './dialog'
|
||||
import disclosure from './disclosure'
|
||||
import listbox from './listbox'
|
||||
import popover from './popover'
|
||||
import menu from './menu'
|
||||
import notSwitch from './switch'
|
||||
import radio from './radio'
|
||||
import tabs from './tabs'
|
||||
|
||||
export default function (Alpine) {
|
||||
combobox(Alpine)
|
||||
dialog(Alpine)
|
||||
disclosure(Alpine)
|
||||
listbox(Alpine)
|
||||
menu(Alpine)
|
||||
notSwitch(Alpine)
|
||||
popover(Alpine)
|
||||
radio(Alpine)
|
||||
tabs(Alpine)
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
|
||||
export function generateContext(Alpine, multiple, orientation, activateSelectedOrFirst) {
|
||||
return {
|
||||
/**
|
||||
* Main state...
|
||||
*/
|
||||
items: [],
|
||||
activeKey: switchboard(),
|
||||
orderedKeys: [],
|
||||
activatedByKeyPress: false,
|
||||
|
||||
/**
|
||||
* Initialization...
|
||||
*/
|
||||
activateSelectedOrFirst: Alpine.debounce(function () {
|
||||
activateSelectedOrFirst(false)
|
||||
}),
|
||||
|
||||
registerItemsQueue: [],
|
||||
|
||||
registerItem(key, el, value, disabled) {
|
||||
// We need to queue up these additions to not slow down the
|
||||
// init process for each row...
|
||||
if (this.registerItemsQueue.length === 0) {
|
||||
queueMicrotask(() => {
|
||||
if (this.registerItemsQueue.length > 0) {
|
||||
this.items = this.items.concat(this.registerItemsQueue)
|
||||
|
||||
this.registerItemsQueue = []
|
||||
|
||||
this.reorderKeys()
|
||||
this.activateSelectedOrFirst()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let item = {
|
||||
key, el, value, disabled
|
||||
}
|
||||
|
||||
this.registerItemsQueue.push(item)
|
||||
},
|
||||
|
||||
unregisterKeysQueue: [],
|
||||
|
||||
unregisterItem(key) {
|
||||
// This gets triggered when the mutation observer picks up DOM changes.
|
||||
// It will get called for every row that gets removed. If there are
|
||||
// 1000x rows, we want to trigger this cleanup when the first one
|
||||
// is handled, let the others add their keys to the queue, then
|
||||
// handle all the cleanup in bulk at the end. Big perf gain...
|
||||
if (this.unregisterKeysQueue.length === 0) {
|
||||
queueMicrotask(() => {
|
||||
if (this.unregisterKeysQueue.length > 0) {
|
||||
this.items = this.items.filter(i => ! this.unregisterKeysQueue.includes(i.key))
|
||||
this.orderedKeys = this.orderedKeys.filter(i => ! this.unregisterKeysQueue.includes(i))
|
||||
|
||||
this.unregisterKeysQueue = []
|
||||
|
||||
this.reorderKeys()
|
||||
this.activateSelectedOrFirst()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.unregisterKeysQueue.push(key)
|
||||
},
|
||||
|
||||
getItemByKey(key) {
|
||||
return this.items.find(i => i.key === key)
|
||||
},
|
||||
|
||||
getItemByValue(value) {
|
||||
return this.items.find(i => Alpine.raw(i.value) === Alpine.raw(value))
|
||||
},
|
||||
|
||||
getItemByEl(el) {
|
||||
return this.items.find(i => i.el === el)
|
||||
},
|
||||
|
||||
getItemsByValues(values) {
|
||||
let rawValues = values.map(i => Alpine.raw(i));
|
||||
let filteredValue = this.items.filter(i => rawValues.includes(Alpine.raw(i.value)))
|
||||
filteredValue = filteredValue.slice().sort((a, b) => {
|
||||
let position = a.el.compareDocumentPosition(b.el)
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
||||
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
||||
return 0
|
||||
})
|
||||
return filteredValue
|
||||
},
|
||||
|
||||
getActiveItem() {
|
||||
if (! this.hasActive()) return null
|
||||
|
||||
let item = this.items.find(i => i.key === this.activeKey.get())
|
||||
|
||||
if (! item) this.deactivateKey(this.activeKey.get())
|
||||
|
||||
return item
|
||||
},
|
||||
|
||||
activateItem(item) {
|
||||
if (! item) return
|
||||
|
||||
this.activateKey(item.key)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle elements...
|
||||
*/
|
||||
reorderKeys: Alpine.debounce(function () {
|
||||
this.orderedKeys = this.items.map(i => i.key)
|
||||
|
||||
this.orderedKeys = this.orderedKeys.slice().sort((a, z) => {
|
||||
if (a === null || z === null) return 0
|
||||
|
||||
let aEl = this.items.find(i => i.key === a).el
|
||||
let zEl = this.items.find(i => i.key === z).el
|
||||
|
||||
let position = aEl.compareDocumentPosition(zEl)
|
||||
|
||||
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
||||
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
||||
return 0
|
||||
})
|
||||
|
||||
// If there no longer is the active key in the items list, then
|
||||
// deactivate it...
|
||||
if (! this.orderedKeys.includes(this.activeKey.get())) this.deactivateKey(this.activeKey.get())
|
||||
}),
|
||||
|
||||
getActiveKey() {
|
||||
return this.activeKey.get()
|
||||
},
|
||||
|
||||
activeEl() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
return this.items.find(i => i.key === this.activeKey.get()).el
|
||||
},
|
||||
|
||||
isActiveEl(el) {
|
||||
let key = this.items.find(i => i.el === el)
|
||||
|
||||
return this.activeKey.is(key)
|
||||
},
|
||||
|
||||
activateEl(el) {
|
||||
let item = this.items.find(i => i.el === el)
|
||||
|
||||
this.activateKey(item.key)
|
||||
},
|
||||
|
||||
isDisabledEl(el) {
|
||||
return this.items.find(i => i.el === el).disabled
|
||||
},
|
||||
|
||||
get isScrollingTo() { return this.scrollingCount > 0 },
|
||||
|
||||
scrollingCount: 0,
|
||||
|
||||
activateAndScrollToKey(key, activatedByKeyPress) {
|
||||
if (! this.getItemByKey(key)) return
|
||||
|
||||
// This addresses the following problem:
|
||||
// If deactivate is hooked up to mouseleave,
|
||||
// scrolling to an element will trigger deactivation.
|
||||
// This "isScrollingTo" is exposed to prevent that.
|
||||
this.scrollingCount++
|
||||
|
||||
this.activateKey(key, activatedByKeyPress)
|
||||
|
||||
let targetEl = this.items.find(i => i.key === key).el
|
||||
|
||||
targetEl.scrollIntoView({ block: 'nearest' })
|
||||
|
||||
setTimeout(() => {
|
||||
this.scrollingCount--
|
||||
// Unfortunately, browser experimentation has shown me
|
||||
// that 25ms is the sweet spot when holding down an
|
||||
// arrow key to scroll the list of items...
|
||||
}, 25)
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle disabled keys...
|
||||
*/
|
||||
isDisabled(key) {
|
||||
let item = this.items.find(i => i.key === key)
|
||||
|
||||
if (! item) return false
|
||||
|
||||
return item.disabled
|
||||
},
|
||||
|
||||
get nonDisabledOrderedKeys() {
|
||||
return this.orderedKeys.filter(i => ! this.isDisabled(i))
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle activated keys...
|
||||
*/
|
||||
hasActive() { return !! this.activeKey.get() },
|
||||
|
||||
/**
|
||||
* Return true if the latest active element was activated
|
||||
* by the user (i.e. using the arrow keys) and false if was
|
||||
* activated automatically by alpine (i.e. first element automatically
|
||||
* activated after filtering the list)
|
||||
*/
|
||||
wasActivatedByKeyPress() {return this.activatedByKeyPress},
|
||||
|
||||
isActiveKey(key) { return this.activeKey.is(key) },
|
||||
|
||||
activateKey(key, activatedByKeyPress = false) {
|
||||
if (this.isDisabled(key)) return
|
||||
|
||||
this.activeKey.set(key)
|
||||
this.activatedByKeyPress = activatedByKeyPress
|
||||
},
|
||||
|
||||
deactivateKey(key) {
|
||||
if (this.activeKey.get() === key) {
|
||||
this.activeKey.set(null)
|
||||
this.activatedByKeyPress = false
|
||||
}
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
if (! this.activeKey.get()) return
|
||||
if (this.isScrollingTo) return
|
||||
|
||||
this.activeKey.set(null)
|
||||
this.activatedByKeyPress = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle active key traversal...
|
||||
*/
|
||||
nextKey() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get())
|
||||
|
||||
return this.nonDisabledOrderedKeys[index + 1]
|
||||
},
|
||||
|
||||
prevKey() {
|
||||
if (! this.activeKey.get()) return
|
||||
|
||||
let index = this.nonDisabledOrderedKeys.findIndex(i => i === this.activeKey.get())
|
||||
|
||||
return this.nonDisabledOrderedKeys[index - 1]
|
||||
},
|
||||
|
||||
firstKey() { return this.nonDisabledOrderedKeys[0] },
|
||||
|
||||
lastKey() { return this.nonDisabledOrderedKeys[this.nonDisabledOrderedKeys.length - 1] },
|
||||
|
||||
searchQuery: '',
|
||||
|
||||
clearSearch: Alpine.debounce(function () { this.searchQuery = '' }, 350),
|
||||
|
||||
searchKey(query) {
|
||||
this.clearSearch()
|
||||
|
||||
this.searchQuery += query
|
||||
|
||||
let foundKey
|
||||
|
||||
for (let key in this.items) {
|
||||
let content = this.items[key].el.textContent.trim().toLowerCase()
|
||||
|
||||
if (content.startsWith(this.searchQuery)) {
|
||||
foundKey = this.items[key].key
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! this.nonDisabledOrderedKeys.includes(foundKey)) return
|
||||
|
||||
return foundKey
|
||||
},
|
||||
|
||||
activateByKeyEvent(e, searchable = false, isOpen = () => false, open = () => {}, setIsTyping) {
|
||||
let targetKey, hasActive
|
||||
|
||||
setIsTyping(true)
|
||||
|
||||
let activatedByKeyPress = true
|
||||
|
||||
switch (e.key) {
|
||||
// case 'Backspace':
|
||||
// case 'Delete':
|
||||
case ['ArrowDown', 'ArrowRight'][orientation === 'vertical' ? 0 : 1]:
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
|
||||
setIsTyping(false)
|
||||
|
||||
if (! isOpen()) {
|
||||
open()
|
||||
break;
|
||||
}
|
||||
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
|
||||
targetKey = hasActive ? this.nextKey() : this.firstKey()
|
||||
break;
|
||||
|
||||
case ['ArrowUp', 'ArrowLeft'][orientation === 'vertical' ? 0 : 1]:
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
|
||||
setIsTyping(false)
|
||||
|
||||
if (! isOpen()) {
|
||||
open()
|
||||
break;
|
||||
}
|
||||
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
|
||||
targetKey = hasActive ? this.prevKey() : this.lastKey()
|
||||
break;
|
||||
case 'Home':
|
||||
case 'PageUp':
|
||||
if (e.key == 'Home' && e.shiftKey) return;
|
||||
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
setIsTyping(false)
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
targetKey = this.firstKey()
|
||||
break;
|
||||
|
||||
case 'End':
|
||||
case 'PageDown':
|
||||
if (e.key == 'End' && e.shiftKey) return;
|
||||
|
||||
e.preventDefault(); e.stopPropagation()
|
||||
setIsTyping(false)
|
||||
this.reorderKeys(); hasActive = this.hasActive()
|
||||
targetKey = this.lastKey()
|
||||
break;
|
||||
|
||||
default:
|
||||
activatedByKeyPress = this.activatedByKeyPress
|
||||
if (searchable && e.key.length === 1) {
|
||||
targetKey = this.searchKey(e.key)
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetKey) {
|
||||
this.activateAndScrollToKey(targetKey, activatedByKeyPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function keyByValue(object, value) {
|
||||
return Object.keys(object).find(key => object[key] === value)
|
||||
}
|
||||
|
||||
export function renderHiddenInputs(Alpine, el, name, value) {
|
||||
// Create input elements...
|
||||
let newInputs = generateInputs(name, value)
|
||||
|
||||
// Mark them for later tracking...
|
||||
newInputs.forEach(i => i._x_hiddenInput = true)
|
||||
|
||||
// Mark them for Alpine ignoring...
|
||||
newInputs.forEach(i => i._x_ignore = true)
|
||||
|
||||
// Gather old elements for removal...
|
||||
let children = el.children
|
||||
|
||||
let oldInputs = []
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i];
|
||||
|
||||
if (child._x_hiddenInput) oldInputs.push(child)
|
||||
else break
|
||||
}
|
||||
|
||||
// Remove old, and insert new ones into the DOM...
|
||||
Alpine.mutateDom(() => {
|
||||
oldInputs.forEach(i => i.remove())
|
||||
|
||||
newInputs.reverse().forEach(i => el.prepend(i))
|
||||
})
|
||||
}
|
||||
|
||||
function generateInputs(name, value, carry = []) {
|
||||
if (isObjectOrArray(value)) {
|
||||
for (let key in value) {
|
||||
carry = carry.concat(
|
||||
generateInputs(`${name}[${key}]`, value[key])
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let el = document.createElement('input')
|
||||
el.setAttribute('type', 'hidden')
|
||||
el.setAttribute('name', name)
|
||||
el.setAttribute('value', '' + value)
|
||||
|
||||
return [el]
|
||||
}
|
||||
|
||||
|
||||
return carry
|
||||
}
|
||||
|
||||
function isObjectOrArray(subject) {
|
||||
return typeof subject === 'object' && subject !== null
|
||||
}
|
||||
|
||||
function switchboard(value) {
|
||||
let lookup = {}
|
||||
|
||||
let current
|
||||
|
||||
let changeTracker = Alpine.reactive({ state: false })
|
||||
|
||||
let get = () => {
|
||||
// Depend on the change tracker so reading "get" becomes reactive...
|
||||
if (changeTracker.state) {
|
||||
//
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
let set = (newValue) => {
|
||||
if (newValue === current) return
|
||||
|
||||
if (current !== undefined) lookup[current].state = false
|
||||
|
||||
current = newValue
|
||||
|
||||
if (lookup[newValue] === undefined) {
|
||||
lookup[newValue] = Alpine.reactive({ state: true })
|
||||
} else {
|
||||
lookup[newValue].state = true
|
||||
}
|
||||
|
||||
changeTracker.state = ! changeTracker.state
|
||||
}
|
||||
|
||||
let is = (comparisonValue) => {
|
||||
if (lookup[comparisonValue] === undefined) {
|
||||
lookup[comparisonValue] = Alpine.reactive({ state: false })
|
||||
return lookup[comparisonValue].state
|
||||
}
|
||||
|
||||
return !! lookup[comparisonValue].state
|
||||
}
|
||||
|
||||
value === undefined || set(value)
|
||||
|
||||
return { get, set, is }
|
||||
}
|
||||
388
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/listbox.js
Normal file
388
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/listbox.js
Normal file
@@ -0,0 +1,388 @@
|
||||
import { generateContext, renderHiddenInputs } from './list-context'
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('listbox', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'options') handleOptions(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('listbox', (el) => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
// @deprecated:
|
||||
get selected() {
|
||||
return data.__value
|
||||
},
|
||||
// @deprecated:
|
||||
get active() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get value() {
|
||||
return data.__value
|
||||
},
|
||||
get isOpen() {
|
||||
return data.__isOpen
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__isDisabled
|
||||
},
|
||||
get activeOption() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.value
|
||||
},
|
||||
get activeIndex() {
|
||||
let active = data.__context.getActiveItem()
|
||||
|
||||
return active && active.key
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('listboxOption', (el) => {
|
||||
let data = Alpine.$data(el)
|
||||
|
||||
// It's not great depending on the existance of the attribute in the DOM
|
||||
// but it's probably the fastest and most reliable at this point...
|
||||
let optionEl = Alpine.findClosest(el, i => {
|
||||
return i.hasAttribute('x-listbox:option')
|
||||
})
|
||||
|
||||
if (! optionEl) throw 'No x-listbox:option directive found...'
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return data.__context.isActiveKey(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
get isSelected() {
|
||||
return data.__isSelected(optionEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return data.__context.isDisabled(Alpine.$data(optionEl).__optionKey)
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-id'() { return ['alpine-listbox-button', 'alpine-listbox-options', 'alpine-listbox-label'] },
|
||||
'x-modelable': '__value',
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
/**
|
||||
* Listbox state...
|
||||
*/
|
||||
__ready: false,
|
||||
__value: null,
|
||||
__isOpen: false,
|
||||
__context: undefined,
|
||||
__isMultiple: undefined,
|
||||
__isStatic: false,
|
||||
__isDisabled: undefined,
|
||||
__compareBy: null,
|
||||
__inputName: null,
|
||||
__orientation: 'vertical',
|
||||
__hold: false,
|
||||
|
||||
/**
|
||||
* Listbox initialization...
|
||||
*/
|
||||
init() {
|
||||
this.__isMultiple = Alpine.extractProp(el, 'multiple', false)
|
||||
this.__isDisabled = Alpine.extractProp(el, 'disabled', false)
|
||||
this.__inputName = Alpine.extractProp(el, 'name', null)
|
||||
this.__compareBy = Alpine.extractProp(el, 'by')
|
||||
this.__orientation = Alpine.extractProp(el, 'horizontal', false) ? 'horizontal' : 'vertical'
|
||||
|
||||
this.__context = generateContext(Alpine, this.__isMultiple, this.__orientation, () => this.__activateSelectedOrFirst())
|
||||
|
||||
let defaultValue = Alpine.extractProp(el, 'default-value', this.__isMultiple ? [] : null)
|
||||
|
||||
this.__value = defaultValue
|
||||
|
||||
// We have to wait again until after the "ready" processes are finished
|
||||
// to settle up currently selected Values (this prevents this next bit
|
||||
// of code from running multiple times on startup...)
|
||||
queueMicrotask(() => {
|
||||
Alpine.effect(() => {
|
||||
// Everytime the value changes, we need to re-render the hidden inputs,
|
||||
// if a user passed the "name" prop...
|
||||
this.__inputName && renderHiddenInputs(Alpine, this.$el, this.__inputName, this.__value)
|
||||
})
|
||||
|
||||
// Keep the currently selected value in sync with the input value...
|
||||
Alpine.effect(() => {
|
||||
this.__resetInput()
|
||||
})
|
||||
})
|
||||
},
|
||||
__resetInput() {
|
||||
let input = this.$refs.__input
|
||||
if (! input) return
|
||||
|
||||
let value = this.$data.__getCurrentValue()
|
||||
|
||||
input.value = value
|
||||
},
|
||||
__getCurrentValue() {
|
||||
if (! this.$refs.__input) return ''
|
||||
if (! this.__value) return ''
|
||||
if (this.$data.__displayValue && this.__value !== undefined) return this.$data.__displayValue(this.__value)
|
||||
if (typeof this.__value === 'string') return this.__value
|
||||
return ''
|
||||
},
|
||||
__open() {
|
||||
if (this.__isOpen) return
|
||||
this.__isOpen = true
|
||||
|
||||
this.__activateSelectedOrFirst()
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => this.$refs.__options.focus({ preventScroll: true }))
|
||||
},
|
||||
__close() {
|
||||
this.__isOpen = false
|
||||
|
||||
this.__context.deactivate()
|
||||
|
||||
this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
|
||||
},
|
||||
__activateSelectedOrFirst(activateSelected = true) {
|
||||
if (! this.__isOpen) return
|
||||
|
||||
if (this.__context.getActiveKey()) {
|
||||
this.__context.activateAndScrollToKey(this.__context.getActiveKey())
|
||||
return
|
||||
}
|
||||
|
||||
let firstSelectedValue
|
||||
|
||||
if (this.__isMultiple) {
|
||||
firstSelectedValue = this.__value.find(i => {
|
||||
return !! this.__context.getItemByValue(i)
|
||||
})
|
||||
} else {
|
||||
firstSelectedValue = this.__value
|
||||
}
|
||||
|
||||
if (activateSelected && firstSelectedValue) {
|
||||
let firstSelected = this.__context.getItemByValue(firstSelectedValue)
|
||||
|
||||
firstSelected && this.__context.activateAndScrollToKey(firstSelected.key)
|
||||
} else {
|
||||
this.__context.activateAndScrollToKey(this.__context.firstKey())
|
||||
}
|
||||
},
|
||||
__selectActive() {
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
if (active) this.__toggleSelected(active.value)
|
||||
},
|
||||
__selectOption(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (item) this.__toggleSelected(item.value)
|
||||
},
|
||||
__isSelected(el) {
|
||||
let item = this.__context.getItemByEl(el)
|
||||
|
||||
if (! item) return false
|
||||
if (item.value === null || item.value === undefined) return false
|
||||
|
||||
return this.__hasSelected(item.value)
|
||||
},
|
||||
__toggleSelected(value) {
|
||||
if (! this.__isMultiple) {
|
||||
this.__value = value
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let index = this.__value.findIndex(j => this.__compare(j, value))
|
||||
|
||||
if (index === -1) {
|
||||
this.__value.push(value)
|
||||
} else {
|
||||
this.__value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
__hasSelected(value) {
|
||||
if (! this.__isMultiple) return this.__compare(this.__value, value)
|
||||
|
||||
return this.__value.some(i => this.__compare(i, value))
|
||||
},
|
||||
__compare(a, b) {
|
||||
let by = this.__compareBy
|
||||
|
||||
if (! by) by = (a, b) => Alpine.raw(a) === Alpine.raw(b)
|
||||
|
||||
if (typeof by === 'string') {
|
||||
let property = by
|
||||
by = (a, b) => {
|
||||
// Handle null values
|
||||
if ((! a || typeof a !== 'object') || (! b || typeof b !== 'object')) {
|
||||
return Alpine.raw(a) === Alpine.raw(b)
|
||||
}
|
||||
|
||||
return a[property] === b[property];
|
||||
}
|
||||
}
|
||||
|
||||
return by(a, b)
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__label',
|
||||
':id'() { return this.$id('alpine-listbox-label') },
|
||||
'@click'() { this.$refs.__button.focus({ preventScroll: true }) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__button',
|
||||
':id'() { return this.$id('alpine-listbox-button') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'aria-haspopup': 'true',
|
||||
':aria-labelledby'() { return this.$id('alpine-listbox-label') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-listbox-options') },
|
||||
|
||||
// Initialize....
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
|
||||
// Register listeners...
|
||||
'@click'() { this.$data.__open() },
|
||||
'@keydown'(e) {
|
||||
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
||||
this.$data.__open()
|
||||
}
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__open() },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOptions(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
// Setup...
|
||||
'x-ref': '__options',
|
||||
':id'() { return this.$id('alpine-listbox-options') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'listbox',
|
||||
tabindex: '0',
|
||||
':aria-orientation'() {
|
||||
return this.$data.__orientation
|
||||
},
|
||||
':aria-labelledby'() { return this.$id('alpine-listbox-button') },
|
||||
':aria-activedescendant'() {
|
||||
if (! this.$data.__context.hasActive()) return
|
||||
|
||||
let active = this.$data.__context.getActiveItem()
|
||||
|
||||
return active ? active.el.id : null
|
||||
},
|
||||
|
||||
// Initialize...
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.extractProp(this.$el, 'static', false)
|
||||
|
||||
if (Alpine.bound(this.$el, 'hold')) {
|
||||
this.$data.__hold = true;
|
||||
}
|
||||
},
|
||||
|
||||
'x-show'() { return this.$data.__isStatic ? true : this.$data.__isOpen },
|
||||
'x-trap'() { return this.$data.__isOpen },
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'@keydown.escape.stop.prevent'() { this.$data.__close() },
|
||||
'@focus'() { this.$data.__activateSelectedOrFirst() },
|
||||
'@keydown'(e) {
|
||||
queueMicrotask(() => this.$data.__context.activateByKeyEvent(e, true, () => this.$data.__isOpen, () => this.$data.__open(), () => {}))
|
||||
},
|
||||
'@keydown.enter.stop.prevent'() {
|
||||
this.$data.__selectActive();
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
'@keydown.space.stop.prevent'() {
|
||||
this.$data.__selectActive();
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, () => {
|
||||
return {
|
||||
'x-id'() { return ['alpine-listbox-option'] },
|
||||
':id'() { return this.$id('alpine-listbox-option') },
|
||||
|
||||
// Accessibility attributes...
|
||||
'role': 'option',
|
||||
':tabindex'() { return this.$listboxOption.isDisabled ? false : '-1' },
|
||||
':aria-selected'() { return this.$listboxOption.isSelected },
|
||||
|
||||
// Initialize...
|
||||
'x-data'() {
|
||||
return {
|
||||
'__optionKey': null,
|
||||
|
||||
init() {
|
||||
this.__optionKey = (Math.random() + 1).toString(36).substring(7)
|
||||
|
||||
let value = Alpine.extractProp(el, 'value')
|
||||
let disabled = Alpine.extractProp(el, 'disabled', false, false)
|
||||
|
||||
this.$data.__context.registerItem(this.__optionKey, el, value, disabled)
|
||||
},
|
||||
destroy() {
|
||||
this.$data.__context.unregisterItem(this.__optionKey)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// Register listeners...
|
||||
'@click'() {
|
||||
if (this.$listboxOption.isDisabled) return;
|
||||
|
||||
this.$data.__selectOption(el)
|
||||
|
||||
this.$data.__isMultiple || this.$data.__close()
|
||||
},
|
||||
'@mouseenter'() { this.$data.__context.activateEl(el) },
|
||||
'@mouseleave'() {
|
||||
this.$data.__hold || this.$data.__context.deactivate()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Little utility to defer a callback into the microtask queue...
|
||||
function microtask(callback) {
|
||||
return new Promise(resolve => queueMicrotask(() => resolve(callback())))
|
||||
}
|
||||
240
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/menu.js
Normal file
240
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/menu.js
Normal file
@@ -0,0 +1,240 @@
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('menu', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'items') handleItems(el, Alpine)
|
||||
else if (directive.value === 'item') handleItem(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('menuItem', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return $data.__activeEl == $data.__itemEl
|
||||
},
|
||||
get isDisabled() {
|
||||
return $data.__itemEl.__isDisabled.value
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-menu-button', 'alpine-menu-items'] },
|
||||
'x-modelable': '__isOpen',
|
||||
'x-data'() {
|
||||
return {
|
||||
__itemEls: [],
|
||||
__activeEl: null,
|
||||
__isOpen: false,
|
||||
__open(activationStrategy) {
|
||||
this.__isOpen = true
|
||||
|
||||
// Safari needs more of a "tick" for focusing after x-show for some reason.
|
||||
// Probably because Alpine adds an extra tick when x-showing for @click.outside
|
||||
let nextTick = callback => requestAnimationFrame(() => requestAnimationFrame(callback))
|
||||
|
||||
nextTick(() => {
|
||||
this.$refs.__items.focus({ preventScroll: true })
|
||||
|
||||
// Activate the first item every time the menu is open...
|
||||
activationStrategy && activationStrategy(Alpine, this.$refs.__items, el => el.__activate())
|
||||
})
|
||||
},
|
||||
__close(focusAfter = true) {
|
||||
this.__isOpen = false
|
||||
|
||||
focusAfter && this.$nextTick(() => this.$refs.__button.focus({ preventScroll: true }))
|
||||
},
|
||||
__contains(outer, inner) {
|
||||
return !! Alpine.findClosest(inner, el => el.isSameNode(outer))
|
||||
}
|
||||
}
|
||||
},
|
||||
'@focusin.window'() {
|
||||
if (! this.$data.__contains(this.$el, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__button',
|
||||
'aria-haspopup': 'true',
|
||||
':aria-labelledby'() { return this.$id('alpine-menu-label') },
|
||||
':id'() { return this.$id('alpine-menu-button') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-menu-items') },
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && ! this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
'@click'() { this.$data.__open() },
|
||||
'@keydown.down.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.up.stop.prevent'() { this.$data.__open(dom.last) },
|
||||
'@keydown.space.stop.prevent'() { this.$data.__open() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__open() },
|
||||
})
|
||||
}
|
||||
|
||||
// When patching children:
|
||||
// The child isn't initialized until it is reached. This is normally fine
|
||||
// except when something like this happens where an "id" is added during the initializing phase
|
||||
// because the "to" element hasn't initialized yet, it doesn't have the ID, so there is a "key" mismatch
|
||||
|
||||
|
||||
function handleItems(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': '__items',
|
||||
'aria-orientation': 'vertical',
|
||||
'role': 'menu',
|
||||
':id'() { return this.$id('alpine-menu-items') },
|
||||
':aria-labelledby'() { return this.$id('alpine-menu-button') },
|
||||
':aria-activedescendant'() { return this.$data.__activeEl && this.$data.__activeEl.id },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
'tabindex': '0',
|
||||
'@click.outside'() { this.$data.__close() },
|
||||
'@keydown'(e) { dom.search(Alpine, this.$refs.__items, e.key, el => el.__activate()) },
|
||||
'@keydown.down.stop.prevent'() {
|
||||
if (this.$data.__activeEl) dom.next(Alpine, this.$data.__activeEl, el => el.__activate())
|
||||
else dom.first(Alpine, this.$refs.__items, el => el.__activate())
|
||||
},
|
||||
'@keydown.up.stop.prevent'() {
|
||||
if (this.$data.__activeEl) dom.previous(Alpine, this.$data.__activeEl, el => el.__activate())
|
||||
else dom.last(Alpine, this.$refs.__items, el => el.__activate())
|
||||
},
|
||||
'@keydown.home.stop.prevent'() { dom.first(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.end.stop.prevent'() { dom.last(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.page-up.stop.prevent'() { dom.first(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.page-down.stop.prevent'() { dom.last(Alpine, this.$refs.__items, el => el.__activate()) },
|
||||
'@keydown.escape.stop.prevent'() { this.$data.__close() },
|
||||
'@keydown.space.stop.prevent'() { this.$data.__activeEl && this.$data.__activeEl.click() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__activeEl && this.$data.__activeEl.click() },
|
||||
// Required for firefox, event.preventDefault() in handleKeyDown for
|
||||
// the Space key doesn't cancel the handleKeyUp, which in turn
|
||||
// triggers a *click*.
|
||||
'@keyup.space.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handleItem(el, Alpine) {
|
||||
Alpine.bind(el, () => {
|
||||
return {
|
||||
'x-data'() {
|
||||
return {
|
||||
__itemEl: this.$el,
|
||||
init() {
|
||||
// Add current element to element list for navigating.
|
||||
let els = Alpine.raw(this.$data.__itemEls)
|
||||
let inserted = false
|
||||
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].compareDocumentPosition(this.$el) & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
els.splice(i, 0, this.$el)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (! inserted) els.push(this.$el)
|
||||
|
||||
this.$el.__activate = () => {
|
||||
this.$data.__activeEl = this.$el
|
||||
this.$el.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
|
||||
this.$el.__deactivate = () => {
|
||||
this.$data.__activeEl = null
|
||||
}
|
||||
|
||||
|
||||
this.$el.__isDisabled = Alpine.reactive({ value: false })
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.$el.__isDisabled.value = Alpine.bound(this.$el, 'disabled', false)
|
||||
})
|
||||
},
|
||||
destroy() {
|
||||
// Remove this element from the elements list.
|
||||
let els = this.$data.__itemEls
|
||||
els.splice(els.indexOf(this.$el), 1)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-menu-item'] },
|
||||
':id'() { return this.$id('alpine-menu-item') },
|
||||
':tabindex'() { return this.__itemEl.__isDisabled.value ? false : '-1' },
|
||||
'role': 'menuitem',
|
||||
'@mousemove'() { this.__itemEl.__isDisabled.value || this.$menuItem.isActive || this.__itemEl.__activate() },
|
||||
'@mouseleave'() { this.__itemEl.__isDisabled.value || ! this.$menuItem.isActive || this.__itemEl.__deactivate() },
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let dom = {
|
||||
first(Alpine, parent, receive = i => i, fallback = () => { }) {
|
||||
let first = Alpine.$data(parent).__itemEls[0]
|
||||
|
||||
if (! first) return fallback()
|
||||
|
||||
if (first.tagName.toLowerCase() === 'template') {
|
||||
return this.next(Alpine, first, receive)
|
||||
}
|
||||
|
||||
if (first.__isDisabled.value) return this.next(Alpine, first, receive)
|
||||
|
||||
return receive(first)
|
||||
},
|
||||
last(Alpine, parent, receive = i => i, fallback = () => { }) {
|
||||
let last = Alpine.$data(parent).__itemEls.slice(-1)[0]
|
||||
|
||||
if (! last) return fallback()
|
||||
if (last.__isDisabled.value) return this.previous(Alpine, last, receive)
|
||||
return receive(last)
|
||||
},
|
||||
next(Alpine, el, receive = i => i, fallback = () => { }) {
|
||||
if (! el) return fallback()
|
||||
|
||||
let els = Alpine.$data(el).__itemEls
|
||||
let next = els[els.indexOf(el) + 1]
|
||||
|
||||
if (! next) return fallback()
|
||||
if (next.__isDisabled.value || next.tagName.toLowerCase() === 'template') return this.next(Alpine, next, receive, fallback)
|
||||
return receive(next)
|
||||
},
|
||||
previous(Alpine, el, receive = i => i, fallback = () => { }) {
|
||||
if (! el) return fallback()
|
||||
|
||||
let els = Alpine.$data(el).__itemEls
|
||||
let prev = els[els.indexOf(el) - 1]
|
||||
|
||||
if (! prev) return fallback()
|
||||
if (prev.__isDisabled.value || prev.tagName.toLowerCase() === 'template') return this.previous(Alpine, prev, receive, fallback)
|
||||
return receive(prev)
|
||||
},
|
||||
searchQuery: '',
|
||||
debouncedClearSearch: undefined,
|
||||
clearSearch(Alpine) {
|
||||
if (! this.debouncedClearSearch) {
|
||||
this.debouncedClearSearch = Alpine.debounce(function () { this.searchQuery = '' }, 350)
|
||||
}
|
||||
|
||||
this.debouncedClearSearch()
|
||||
},
|
||||
search(Alpine, parent, key, receiver) {
|
||||
if (key.length > 1) return
|
||||
|
||||
this.searchQuery += key
|
||||
|
||||
let els = Alpine.raw(Alpine.$data(parent).__itemEls)
|
||||
|
||||
let el = els.find(el => {
|
||||
return el.textContent.trim().toLowerCase().startsWith(this.searchQuery)
|
||||
})
|
||||
|
||||
el && ! el.__isDisabled.value && receiver(el)
|
||||
|
||||
this.clearSearch(Alpine)
|
||||
},
|
||||
}
|
||||
209
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/popover.js
Normal file
209
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/popover.js
Normal file
@@ -0,0 +1,209 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('popover', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'overlay') handleOverlay(el, Alpine)
|
||||
else if (directive.value === 'button') handleButton(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
else if (directive.value === 'group') handleGroup(el, Alpine)
|
||||
})
|
||||
|
||||
Alpine.magic('popover', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isOpen() {
|
||||
return $data.__isOpenState
|
||||
},
|
||||
open() {
|
||||
$data.__open()
|
||||
},
|
||||
close() {
|
||||
$data.__close()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-popover-button', 'alpine-popover-panel'] },
|
||||
'x-modelable': '__isOpenState',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
if (this.$data.__groupEl) {
|
||||
this.$data.__groupEl.addEventListener('__close-others', ({ detail }) => {
|
||||
if (detail.el.isSameNode(this.$el)) return
|
||||
|
||||
this.__close(false)
|
||||
})
|
||||
}
|
||||
},
|
||||
__buttonEl: undefined,
|
||||
__panelEl: undefined,
|
||||
__isStatic: false,
|
||||
get __isOpen() {
|
||||
if (this.__isStatic) return true
|
||||
|
||||
return this.__isOpenState
|
||||
},
|
||||
__isOpenState: false,
|
||||
__open() {
|
||||
this.__isOpenState = true
|
||||
|
||||
this.$dispatch('__close-others', { el: this.$el })
|
||||
},
|
||||
__toggle() {
|
||||
this.__isOpenState ? this.__close() : this.__open()
|
||||
},
|
||||
__close(el) {
|
||||
if (this.__isStatic) return
|
||||
|
||||
this.__isOpenState = false
|
||||
|
||||
if (el === false) return
|
||||
|
||||
el = el || this.$data.__buttonEl
|
||||
|
||||
if (document.activeElement.isSameNode(el)) return
|
||||
|
||||
setTimeout(() => el.focus())
|
||||
},
|
||||
__contains(outer, inner) {
|
||||
return !! Alpine.findClosest(inner, el => el.isSameNode(outer))
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keydown.escape.stop.prevent'() {
|
||||
this.__close()
|
||||
},
|
||||
'@focusin.window'() {
|
||||
if (this.$data.__groupEl) {
|
||||
if (! this.$data.__contains(this.$data.__groupEl, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (! this.$data.__contains(this.$el, document.activeElement)) {
|
||||
this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleButton(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': 'button',
|
||||
':id'() { return this.$id('alpine-popover-button') },
|
||||
':aria-expanded'() { return this.$data.__isOpen },
|
||||
':aria-controls'() { return this.$data.__isOpen && this.$id('alpine-popover-panel') },
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
|
||||
this.$data.__buttonEl = this.$el
|
||||
},
|
||||
'@click'() { this.$data.__toggle() },
|
||||
'@keydown.tab'(e) {
|
||||
if (! e.shiftKey && this.$data.__isOpen) {
|
||||
let firstFocusableEl = this.$focus.within(this.$data.__panelEl).getFirst()
|
||||
|
||||
if (firstFocusableEl) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.$focus.focus(firstFocusableEl)
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keyup.tab'(e) {
|
||||
if (this.$data.__isOpen) {
|
||||
// Check if the last focused element was "after" this one
|
||||
let lastEl = this.$focus.previouslyFocused()
|
||||
|
||||
if (! lastEl) return
|
||||
|
||||
if (
|
||||
// Make sure the last focused wasn't part of this popover.
|
||||
(! this.$data.__buttonEl.contains(lastEl) && ! this.$data.__panelEl.contains(lastEl))
|
||||
// Also make sure it appeared "after" this button in the DOM.
|
||||
&& (lastEl && (this.$el.compareDocumentPosition(lastEl) & Node.DOCUMENT_POSITION_FOLLOWING))
|
||||
) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.$focus.within(this.$data.__panelEl).last()
|
||||
}
|
||||
}
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__toggle() },
|
||||
'@keydown.enter.stop.prevent'() { this.$data.__toggle() },
|
||||
// This is to stop Firefox from firing a "click".
|
||||
'@keyup.space.stop.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() {
|
||||
this.$data.__isStatic = Alpine.bound(this.$el, 'static', false)
|
||||
this.$data.__panelEl = this.$el
|
||||
},
|
||||
'x-effect'() {
|
||||
this.$data.__isOpen && Alpine.bound(el, 'focus') && this.$focus.first()
|
||||
},
|
||||
'x-ref': 'panel',
|
||||
':id'() { return this.$id('alpine-popover-panel') },
|
||||
'x-show'() { return this.$data.__isOpen },
|
||||
'@mousedown.window'($event) {
|
||||
if (! this.$data.__isOpen) return
|
||||
if (this.$data.__contains(this.$data.__buttonEl, $event.target)) return
|
||||
if (this.$data.__contains(this.$el, $event.target)) return
|
||||
|
||||
if (! this.$focus.focusable($event.target)) {
|
||||
this.$data.__close()
|
||||
}
|
||||
},
|
||||
'@keydown.tab'(e) {
|
||||
if (e.shiftKey && this.$focus.isFirst(e.target)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
Alpine.bound(el, 'focus') ? this.$data.__close() : this.$data.__buttonEl.focus()
|
||||
} else if (! e.shiftKey && this.$focus.isLast(e.target)) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// Get the next panel button:
|
||||
let els = this.$focus.within(document).all()
|
||||
let buttonIdx = els.indexOf(this.$data.__buttonEl)
|
||||
|
||||
let nextEls = els
|
||||
.splice(buttonIdx + 1) // Elements after button
|
||||
.filter(el => ! this.$el.contains(el)) // Ignore items in panel
|
||||
|
||||
nextEls[0].focus()
|
||||
|
||||
Alpine.bound(el, 'focus') && this.$data.__close(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleGroup(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-ref': 'container',
|
||||
'x-data'() {
|
||||
return {
|
||||
__groupEl: this.$el,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOverlay(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-show'() { return this.$data.__isOpen }
|
||||
})
|
||||
}
|
||||
220
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/radio.js
Normal file
220
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/radio.js
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('radio', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'option') handleOption(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('radioOption', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isActive() {
|
||||
return $data.__option === $data.__active
|
||||
},
|
||||
get isChecked() {
|
||||
return $data.__option === $data.__value
|
||||
},
|
||||
get isDisabled() {
|
||||
let disabled = $data.__disabled
|
||||
|
||||
if ($data.__rootDisabled) return true
|
||||
|
||||
return disabled
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__value',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__rootDisabled = Alpine.bound(el, 'disabled', false);
|
||||
this.__value = Alpine.bound(this.$el, 'default-value', false)
|
||||
this.__inputName = Alpine.bound(this.$el, 'name', false)
|
||||
this.__inputId = 'alpine-radio-'+Date.now()
|
||||
})
|
||||
|
||||
// Add `role="none"` to all non role elements.
|
||||
this.$nextTick(() => {
|
||||
let walker = document.createTreeWalker(
|
||||
this.$el,
|
||||
NodeFilter.SHOW_ELEMENT,
|
||||
{
|
||||
acceptNode: node => {
|
||||
if (node.getAttribute('role') === 'radio') return NodeFilter.FILTER_REJECT
|
||||
if (node.hasAttribute('role')) return NodeFilter.FILTER_SKIP
|
||||
return NodeFilter.FILTER_ACCEPT
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
|
||||
while (walker.nextNode()) walker.currentNode.setAttribute('role', 'none')
|
||||
})
|
||||
},
|
||||
__value: undefined,
|
||||
__active: undefined,
|
||||
__rootEl: this.$el,
|
||||
__optionValues: [],
|
||||
__disabledOptions: new Set,
|
||||
__optionElsByValue: new Map,
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
__rootDisabled: false,
|
||||
__inputName: undefined,
|
||||
__inputId: undefined,
|
||||
__change(value) {
|
||||
if (this.__rootDisabled) return
|
||||
|
||||
this.__value = value
|
||||
},
|
||||
__addOption(option, el, disabled) {
|
||||
// Add current element to element list for navigating.
|
||||
let options = Alpine.raw(this.__optionValues)
|
||||
let els = options.map(i => this.__optionElsByValue.get(i))
|
||||
let inserted = false
|
||||
|
||||
for (let i = 0; i < els.length; i++) {
|
||||
if (els[i].compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
options.splice(i, 0, option)
|
||||
this.__optionElsByValue.set(option, el)
|
||||
inserted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!inserted) {
|
||||
options.push(option)
|
||||
this.__optionElsByValue.set(option, el)
|
||||
}
|
||||
|
||||
disabled && this.__disabledOptions.add(option)
|
||||
},
|
||||
__isFirstOption(option) {
|
||||
return this.__optionValues.indexOf(option) === 0
|
||||
},
|
||||
__setActive(option) {
|
||||
this.__active = option
|
||||
},
|
||||
__focusOptionNext() {
|
||||
let option = this.__active
|
||||
let all = this.__optionValues.filter(i => !this.__disabledOptions.has(i))
|
||||
let next = all[this.__optionValues.indexOf(option) + 1]
|
||||
next = next || all[0]
|
||||
|
||||
this.__optionElsByValue.get(next).focus()
|
||||
this.__change(next)
|
||||
},
|
||||
__focusOptionPrev() {
|
||||
let option = this.__active
|
||||
let all = this.__optionValues.filter(i => !this.__disabledOptions.has(i))
|
||||
let prev = all[all.indexOf(option) - 1]
|
||||
prev = prev || all.slice(-1)[0]
|
||||
|
||||
this.__optionElsByValue.get(prev).focus()
|
||||
this.__change(prev)
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-effect'() {
|
||||
let value = this.__value
|
||||
|
||||
// Only render a hidden input if the "name" prop is passed...
|
||||
if (! this.__inputName) return
|
||||
|
||||
// First remove a previously appended hidden input (if it exists)...
|
||||
let nextEl = this.$el.nextElementSibling
|
||||
if (nextEl && String(nextEl.id) === String(this.__inputId)) {
|
||||
nextEl.remove()
|
||||
}
|
||||
|
||||
// If the value is true, create the input and append it, otherwise,
|
||||
// we already removed it in the previous step...
|
||||
if (value) {
|
||||
let input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.value = value
|
||||
input.name = this.__inputName
|
||||
input.id = this.__inputId
|
||||
|
||||
this.$el.after(input)
|
||||
}
|
||||
},
|
||||
'role': 'radiogroup',
|
||||
'x-id'() { return ['alpine-radio-label', 'alpine-radio-description'] },
|
||||
':aria-labelledby'() { return this.__hasLabel && this.$id('alpine-radio-label') },
|
||||
':aria-describedby'() { return this.__hasDescription && this.$id('alpine-radio-description') },
|
||||
'@keydown.up.prevent.stop'() { this.__focusOptionPrev() },
|
||||
'@keydown.left.prevent.stop'() { this.__focusOptionPrev() },
|
||||
'@keydown.down.prevent.stop'() { this.__focusOptionNext() },
|
||||
'@keydown.right.prevent.stop'() { this.__focusOptionNext() },
|
||||
})
|
||||
}
|
||||
|
||||
function handleOption(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__disabled = Alpine.bound(el, 'disabled', false)
|
||||
this.__option = Alpine.bound(el, 'value')
|
||||
this.$data.__addOption(this.__option, this.$el, this.__disabled)
|
||||
})
|
||||
},
|
||||
__option: undefined,
|
||||
__disabled: false,
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
}
|
||||
},
|
||||
'x-id'() { return ['alpine-radio-label', 'alpine-radio-description'] },
|
||||
'role': 'radio',
|
||||
':aria-checked'() { return this.$radioOption.isChecked },
|
||||
':aria-disabled'() { return this.$radioOption.isDisabled },
|
||||
':aria-labelledby'() { return this.__hasLabel && this.$id('alpine-radio-label') },
|
||||
':aria-describedby'() { return this.__hasDescription && this.$id('alpine-radio-description') },
|
||||
':tabindex'() {
|
||||
if (this.$radioOption.isDisabled) return -1
|
||||
if (this.$radioOption.isChecked) return 0
|
||||
if (! this.$data.__value && this.$data.__isFirstOption(this.$data.__option)) return 0
|
||||
|
||||
return -1
|
||||
},
|
||||
'@click'() {
|
||||
if (this.$radioOption.isDisabled) return
|
||||
this.$data.__change(this.$data.__option)
|
||||
this.$el.focus()
|
||||
},
|
||||
'@focus'() {
|
||||
if (this.$radioOption.isDisabled) return
|
||||
this.$data.__setActive(this.$data.__option)
|
||||
},
|
||||
'@blur'() {
|
||||
if (this.$data.__active === this.$data.__option) this.$data.__setActive(undefined)
|
||||
},
|
||||
'@keydown.space.stop.prevent'() { this.$data.__change(this.$data.__option) },
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasLabel = true },
|
||||
':id'() { return this.$id('alpine-radio-label') },
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasDescription = true },
|
||||
':id'() { return this.$id('alpine-radio-description') },
|
||||
})
|
||||
}
|
||||
116
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/switch.js
Normal file
116
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/switch.js
Normal file
@@ -0,0 +1,116 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('switch', (el, directive) => {
|
||||
if (directive.value === 'group') handleGroup(el, Alpine)
|
||||
else if (directive.value === 'label') handleLabel(el, Alpine)
|
||||
else if (directive.value === 'description') handleDescription(el, Alpine)
|
||||
else handleRoot(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('switch', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isChecked() {
|
||||
return $data.__value === true
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleGroup(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-id'() { return ['alpine-switch-label', 'alpine-switch-description'] },
|
||||
'x-data'() {
|
||||
return {
|
||||
__hasLabel: false,
|
||||
__hasDescription: false,
|
||||
__switchEl: undefined,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__value',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
this.__value = Alpine.bound(this.$el, 'default-checked', false)
|
||||
this.__inputName = Alpine.bound(this.$el, 'name', false)
|
||||
this.__inputValue = Alpine.bound(this.$el, 'value', 'on')
|
||||
this.__inputId = 'alpine-switch-'+Date.now()
|
||||
})
|
||||
},
|
||||
__value: undefined,
|
||||
__inputName: undefined,
|
||||
__inputValue: undefined,
|
||||
__inputId: undefined,
|
||||
__toggle() {
|
||||
this.__value = ! this.__value;
|
||||
},
|
||||
}
|
||||
},
|
||||
'x-effect'() {
|
||||
let value = this.__value
|
||||
|
||||
// Only render a hidden input if the "name" prop is passed...
|
||||
if (! this.__inputName) return
|
||||
|
||||
// First remove a previously appended hidden input (if it exists)...
|
||||
let nextEl = this.$el.nextElementSibling
|
||||
if (nextEl && String(nextEl.id) === String(this.__inputId)) {
|
||||
nextEl.remove()
|
||||
}
|
||||
|
||||
// If the value is true, create the input and append it, otherwise,
|
||||
// we already removed it in the previous step...
|
||||
if (value) {
|
||||
let input = document.createElement('input')
|
||||
|
||||
input.type = 'hidden'
|
||||
input.value = this.__inputValue
|
||||
input.name = this.__inputName
|
||||
input.id = this.__inputId
|
||||
|
||||
this.$el.after(input)
|
||||
}
|
||||
},
|
||||
'x-init'() {
|
||||
if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
|
||||
this.$data.__switchEl = this.$el
|
||||
},
|
||||
'role': 'switch',
|
||||
'tabindex': "0",
|
||||
':aria-checked'() { return !!this.__value },
|
||||
':aria-labelledby'() { return this.$data.__hasLabel && this.$id('alpine-switch-label') },
|
||||
':aria-describedby'() { return this.$data.__hasDescription && this.$id('alpine-switch-description') },
|
||||
'@click.prevent'() { this.__toggle() },
|
||||
'@keyup'(e) {
|
||||
if (e.key !== 'Tab') e.preventDefault()
|
||||
if (e.key === ' ') this.__toggle()
|
||||
},
|
||||
// This is needed so that we can "cancel" the click event when we use the `Enter` key on a button.
|
||||
'@keypress.prevent'() { },
|
||||
})
|
||||
}
|
||||
|
||||
function handleLabel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasLabel = true },
|
||||
':id'() { return this.$id('alpine-switch-label') },
|
||||
'@click'() {
|
||||
this.$data.__switchEl.click()
|
||||
this.$data.__switchEl.focus({ preventScroll: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleDescription(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__hasDescription = true },
|
||||
':id'() { return this.$id('alpine-switch-description') },
|
||||
})
|
||||
}
|
||||
141
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/tabs.js
Normal file
141
themes/hugo-mod-jslibs-dist/alpinejs/packages/ui/src/tabs.js
Normal file
@@ -0,0 +1,141 @@
|
||||
|
||||
export default function (Alpine) {
|
||||
Alpine.directive('tabs', (el, directive) => {
|
||||
if (! directive.value) handleRoot(el, Alpine)
|
||||
else if (directive.value === 'list') handleList(el, Alpine)
|
||||
else if (directive.value === 'tab') handleTab(el, Alpine)
|
||||
else if (directive.value === 'panels') handlePanels(el, Alpine)
|
||||
else if (directive.value === 'panel') handlePanel(el, Alpine)
|
||||
}).before('bind')
|
||||
|
||||
Alpine.magic('tab', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isSelected() {
|
||||
return $data.__selectedIndex === $data.__tabs.indexOf($data.__tabEl)
|
||||
},
|
||||
get isDisabled() {
|
||||
return $data.__isDisabled
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Alpine.magic('panel', el => {
|
||||
let $data = Alpine.$data(el)
|
||||
|
||||
return {
|
||||
get isSelected() {
|
||||
return $data.__selectedIndex === $data.__panels.indexOf($data.__panelEl)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRoot(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-modelable': '__selectedIndex',
|
||||
'x-data'() {
|
||||
return {
|
||||
init() {
|
||||
queueMicrotask(() => {
|
||||
let defaultIndex = this.__selectedIndex || Number(Alpine.bound(this.$el, 'default-index', 0))
|
||||
let tabs = this.__activeTabs()
|
||||
let clamp = (number, min, max) => Math.min(Math.max(number, min), max)
|
||||
|
||||
this.__selectedIndex = clamp(defaultIndex, 0, tabs.length -1)
|
||||
|
||||
Alpine.effect(() => {
|
||||
this.__manualActivation = Alpine.bound(this.$el, 'manual', false)
|
||||
})
|
||||
})
|
||||
},
|
||||
__tabs: [],
|
||||
__panels: [],
|
||||
__selectedIndex: null,
|
||||
__tabGroupEl: undefined,
|
||||
__manualActivation: false,
|
||||
__addTab(el) { this.__tabs.push(el) },
|
||||
__addPanel(el) { this.__panels.push(el) },
|
||||
__selectTab(el) {
|
||||
this.__selectedIndex = this.__tabs.indexOf(el)
|
||||
},
|
||||
__activeTabs() {
|
||||
return this.__tabs.filter(i => !i.__disabled)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleList(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { this.$data.__tabGroupEl = this.$el }
|
||||
})
|
||||
}
|
||||
|
||||
function handleTab(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
'x-init'() { if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button' },
|
||||
'x-data'() { return {
|
||||
init() {
|
||||
this.__tabEl = this.$el
|
||||
this.$data.__addTab(this.$el)
|
||||
this.__tabEl.__disabled = Alpine.bound(this.$el, 'disabled', false)
|
||||
this.__isDisabled = this.__tabEl.__disabled
|
||||
},
|
||||
__tabEl: undefined,
|
||||
__isDisabled: false,
|
||||
}},
|
||||
'@click'() {
|
||||
if (this.$el.__disabled) return
|
||||
|
||||
this.$data.__selectTab(this.$el)
|
||||
|
||||
this.$el.focus()
|
||||
},
|
||||
'@keydown.enter.prevent.stop'() { this.__selectTab(this.$el) },
|
||||
'@keydown.space.prevent.stop'() { this.__selectTab(this.$el) },
|
||||
'@keydown.home.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
|
||||
'@keydown.page-up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).first() },
|
||||
'@keydown.end.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
|
||||
'@keydown.page-down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).last() },
|
||||
'@keydown.down.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
|
||||
'@keydown.right.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().next() },
|
||||
'@keydown.up.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
|
||||
'@keydown.left.prevent.stop'() { this.$focus.within(this.$data.__activeTabs()).withWrapAround().prev() },
|
||||
':tabindex'() { return this.$tab.isSelected ? 0 : -1 },
|
||||
'@focus'() {
|
||||
if (this.$data.__manualActivation) {
|
||||
this.$el.focus()
|
||||
} else {
|
||||
if (this.$el.__disabled) return
|
||||
|
||||
this.$data.__selectTab(this.$el)
|
||||
|
||||
this.$el.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanels(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
//
|
||||
})
|
||||
}
|
||||
|
||||
function handlePanel(el, Alpine) {
|
||||
Alpine.bind(el, {
|
||||
':tabindex'() { return this.$panel.isSelected ? 0 : -1 },
|
||||
'x-data'() { return {
|
||||
init() {
|
||||
this.__panelEl = this.$el
|
||||
this.$data.__addPanel(this.$el)
|
||||
},
|
||||
__panelEl: undefined,
|
||||
}},
|
||||
'x-show'() { return this.$panel.isSelected },
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user