Add supporting themes required for Lotusdocs

This commit is contained in:
Abner Coimbre
2026-01-11 16:48:19 -08:00
parent 8a4d04db58
commit f8d40c4e41
1289 changed files with 234948 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import ui from '../src/index.js'
document.addEventListener('alpine:init', () => {
window.Alpine.plugin(ui)
})

View File

@@ -0,0 +1,5 @@
import ui from '../src/index.js'
export default ui
export { ui }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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": {}
}

View 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())))
}

View File

@@ -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') },
})
}

View File

@@ -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')
},
})
}

View File

@@ -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)
}

View File

@@ -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 }
}

View 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())))
}

View 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)
},
}

View 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 }
})
}

View 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') },
})
}

View 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') },
})
}

View 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 },
})
}