Skip to content

Instantly share code, notes, and snippets.

@mplatts
Last active July 25, 2022 05:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mplatts/038d1bea5cdd33aea956ef9103c35168 to your computer and use it in GitHub Desktop.
Save mplatts/038d1bea5cdd33aea956ef9103c35168 to your computer and use it in GitHub Desktop.
Combo box HEEX + Alpine JS
<button
@click.prevent="openDocs()"
class="items-center hidden w-full px-4 mr-auto border rounded-lg dark:border-gray-600 dark:bg-gray-900 dark:hover:bg-gray-800/30 bg-slate-100 hover:bg-slate-50 md:flex group"
>
<div class="mr-2 text-gray-400">
<svg
class="w-5 h-5"
viewbox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.7 19.3L17 15.6C20.1 11.7 19.5 6 15.6 2.9C11.7 -0.2 5.99999 0.5 2.89999 4.3C-0.200006 8.2 0.499995 13.9 4.29999 17C7.59999 19.6 12.3 19.6 15.6 17L19.3 20.7C19.7 21.1 20.3 21.1 20.7 20.7C21.1 20.3 21.1 19.7 20.7 19.3ZM9.99999 17C6.09999 17 2.99999 13.9 2.99999 10C2.99999 6.1 6.09999 3 9.99999 3C13.9 3 17 6.1 17 10C17 13.9 13.9 17 9.99999 17Z"
fill="currentColor"
>
</path>
</svg>
</div>
<div class="flex justify-between w-full text-gray-400">
<div class="py-3 pl-2">
Search
</div>
<div class="flex px-3 py-2 my-2 text-xs border border-gray-300 rounded dark:border-gray-600 dark:bg-gray-800">
<div class="">
</div>
<div class="">
K
</div>
</div>
</div>
</button>
defmodule PetalPro.DocsHelpers do
alias PetalProWeb.Router.Helpers, as: Routes
alias PetalProWeb.Endpoint
def nav_items do
[
%{
label: "Install",
to: Routes.docs_path(Endpoint, :install),
name: :install,
contents: [
%{label: "New project", id: "install_new_project"},
%{label: "How to install Tailwind", id: "install_tailwind"},
%{label: "How to install Petal Components", id: "install_petal_components"},
%{label: "How to install VSCode snippets", id: "install_vscode_snippets"}
]
},
%{
label: "VSCode snippets",
to: Routes.docs_path(Endpoint, :vscode_snippets),
name: :vscode_snippets,
contents: [
%{label: "VSCode Snippets Cheatsheet", id: "vscode_snippets_cheatsheet"}
]
},
%{
label: "Containers",
to: Routes.docs_path(Endpoint, :containers),
name: :containers,
contents: [
%{label: "Sizes", id: "sizes"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Links",
to: Routes.docs_path(Endpoint, :links),
name: :links,
contents: [
%{label: "Types", id: "link_types"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Buttons",
to: Routes.docs_path(Endpoint, :buttons),
name: :buttons,
contents: [
%{label: "Types", id: "button_types"},
%{label: "Colors", id: "button_colors"},
%{label: "Outline", id: "button_colors_outline"},
%{label: "Inverted", id: "button_colors_inverted"},
%{label: "Sizes", id: "button_sizes"},
%{label: "States", id: "button_states"},
%{label: "With icon", id: "button_with_icon"},
%{label: "Icon buttons", id: "icon_buttons"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Typography",
to: Routes.docs_path(Endpoint, :typography),
name: :typography,
contents: [
%{label: "Headings", id: "headings"},
%{label: "Paragraphs", id: "paragraphs"},
%{label: "Lists", id: "lists"},
%{label: "Prose", id: "prose"}
]
},
%{
label: "Heroicons",
to: Routes.docs_path(Endpoint, :heroicons),
name: :heroicons,
contents: [
%{label: "Solid", id: "solid"},
%{label: "Outline", id: "outline"}
]
},
%{
label: "Badges",
to: Routes.docs_path(Endpoint, :badges),
name: :badges,
contents: [
%{label: "Light", id: "light"},
%{label: "Dark", id: "dark"},
%{label: "Outline", id: "outline"},
%{label: "With icon", id: "with_icon"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Alerts",
to: Routes.docs_path(Endpoint, :alerts),
name: :alerts,
contents: [
%{label: "Default", id: "default"},
%{label: "With icon", id: "with_icon"},
%{label: "Dismissable", id: "dismissable"},
%{label: "With optional heading", id: "optional_heading"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Forms",
to: Routes.docs_path(Endpoint, :forms),
name: :forms,
contents: [
%{label: "Text input", id: "text_input"},
%{label: "Email input", id: "email_input"},
%{label: "Number input", id: "number_input"},
%{label: "Password input", id: "password_input"},
%{label: "Search input", id: "search_input"},
%{label: "Telephone input", id: "telephone_input"},
%{label: "URL input", id: "url_input"},
%{label: "Time input", id: "time_input"},
%{label: "Time select", id: "time_select"},
%{label: "Datetime local input", id: "datetime_local_input"},
%{label: "Datetime select", id: "datetime_select"},
%{label: "Color input", id: "color_input"},
%{label: "File input", id: "file_input"},
%{label: "Range input", id: "range_input"},
%{label: "Textarea", id: "textarea"},
%{label: "Select", id: "select"},
%{label: "Multiple Select", id: "multiple_select"},
%{label: "Switch", id: "switch"},
%{label: "Checkbox", id: "checkbox"},
%{label: "Checkbox group", id: "checkbox_group"},
%{label: "Radio", id: "radio"},
%{label: "Labels", id: "form_label"},
%{label: "Errors", id: "form_field_error"}
]
},
%{
label: "Dropdowns",
to: Routes.docs_path(Endpoint, :dropdowns),
name: :dropdowns,
contents: [
%{label: "Basic dropdown", id: "basic_dropdown"},
%{label: "Unlabelled dropdown", id: "unlabelled_dropdown"},
%{label: "Basic dropdown", id: "basic_dropdown"},
%{label: "Custom dropdown trigger", id: "custom_dropdown_trigger"}
]
},
%{
label: "Loading",
to: Routes.docs_path(Endpoint, :loading),
name: :loading,
contents: [
%{label: "Spinner", id: "spinner"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Breadcrumbs",
to: Routes.docs_path(Endpoint, :breadcrumbs),
name: :breadcrumbs,
contents: [
%{label: "Slash", id: "slash"},
%{label: "Chevron", id: "chevron"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Avatars",
to: Routes.docs_path(Endpoint, :avatars),
name: :avatars,
contents: [
%{label: "Image avatars", id: "image_avatars"},
%{label: "Avatars with placeholder icon", id: "avatars_with_placeholder_icon"},
%{label: "Avatars with placeholder initials", id: "avatars_with_placeholder_initials"},
%{
label: "Colored avatars with placeholder initials",
id: "colored_avatars_with_placeholder_initials"
},
%{label: "Avatar groups stacked", id: "avatar_groups_stacked"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Progress",
to: Routes.docs_path(Endpoint, :progress),
name: :progress,
contents: [
%{label: "Progress bars", id: "progress_bars"},
%{label: "Sizes", id: "sizes"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Pagination",
to: Routes.docs_path(Endpoint, :pagination),
name: :pagination,
contents: [
%{label: "Basic pagination", id: "basic_pagination"},
%{label: "Interactive pagination", id: "interactive_pagination"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Modals",
to: Routes.docs_path(Endpoint, :modals),
name: :modals,
contents: [
%{label: "Sizes", id: "sizes"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Tabs",
to: Routes.docs_path(Endpoint, :tabs),
name: :tabs,
contents: [
%{label: "Basic tabs", id: "basic_tabs"},
%{label: "Tabs with underline", id: "tabs_with_underline"},
%{label: "Basic tabs with number", id: "basic_tabs_with_number"},
%{label: "Tabs underlined with number", id: "tabs_underlined_with_number"},
%{label: "Basic tabs with icons", id: "basic_tabs_with_icons"},
%{
label: "Tabs underlined with outline icons",
id: "tabs_underlined_with_outline_icons"
},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Card",
to: Routes.docs_path(Endpoint, :card),
name: :card,
contents: [
%{label: "Basic cards", id: "basic_cards"},
%{label: "Outline cards", id: "outline_cards"},
%{label: "Cards with media", id: "cards_with_media"},
%{label: "Cards with media and footer", id: "cards_with_media_and_footer"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Table",
to: Routes.docs_path(Endpoint, :table),
name: :table,
contents: [
%{label: "Basic table", id: "basic_table"},
%{
label: "Multi-lined table with avatars and badges",
id: "multi-lined_table_with_avatars_and_badges"
},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Slide Over",
to: Routes.docs_path(Endpoint, :slide_over),
name: :slide_over,
contents: [
%{label: "Origins", id: "origins"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Accordion",
to: Routes.docs_path(Endpoint, :accordion),
name: :accordion,
contents: [
%{label: "Basic Accordion", id: "basic_accordion"},
%{label: "Accordion with alternative icon", id: "accordion_alternative_icon"},
%{label: "Properties", id: "properties"}
]
},
%{
label: "Modifying components",
to: Routes.docs_path(Endpoint, :modifying_components),
name: :modifying_components,
contents: [
%{label: "Modifying components", id: "modifying_components"}
]
}
]
end
def nav_items_json() do
array_items =
nav_items()
|> Enum.map(fn item ->
Enum.map(item.contents, fn sub_item ->
"{category: '#{item.label}', label: '#{sub_item.label}', path: '#{item.to}##{sub_item.id}'},"
end)
end)
|> List.flatten()
|> Enum.join(" ")
"[#{array_items}]"
end
end
<html lang="en" class="w-full h-full scroll-smooth">
<%= render("_head.html", assigns) %>
<script src="https://cdn.jsdelivr.net/npm/fuzzy@0.1.3/lib/fuzzy.js">
</script>
<script>
window.docSearch = function (options) {
return {
searchOpen: false,
filter: "",
results: [],
selected: null,
options: options,
init() {
let self = this;
this.$watch("filter", function(e) {
self.selected = 0;
self.setResults();
})
// As we hit the down/up buttons and options are being highlighted,
// we need to scroll them into view if they're outside the view
this.$watch("selected", function(e) {
self.$nextTick(function() {
self.getSelectedEl().scrollIntoView({
behavior: "smooth",
block: "nearest",
});
})
})
this.setResults();
},
closeDocs() {
document.querySelectorAll('.phx-click-loading').forEach(el => el.classList.remove('phx-click-loading'));
this.searchOpen = false;
this.selected = 0;
},
openDocs() {
let self = this;
this.searchOpen = true;
this.filter = "";
this.$nextTick(() => self.$refs.searchInput.focus());
},
toggleDocs() {
if (this.searchOpen) {
this.closeDocs();
} else {
this.openDocs();
}
},
isOpen() {
return this.searchOpen === true;
},
classOption(path, index) {
const isSelected = this.selected == index;
return {
"hover:bg-primary-50 group rounded": true,
"bg-primary-100 dark:bg-gray-800": isSelected,
};
},
setResults() {
let results = fuzzy.filter(this.filter, this.options, {
extract: function(el) { return `${el.category} ${el.label}`; }
});
this.results = this.options
? results.map(r => r.original)
: [];
},
onOptionClick(index) {
this.selected = index;
this.selectOption();
},
selectOption() {
this.filter = '';
this.selected = this.selected ?? 0;
this.getSelectedEl().click();
this.closeDocs();
},
getSelectedEl() {
const selected = this.results[this.selected];
return document.querySelector(`a[href='${selected.path}']`);
},
focusPrevOption() {
let newIndex = this.selected - 1;
newIndex < 0 && (newIndex = this.results.length - 1),
this.selected = newIndex
},
focusNextOption() {
let newIndex = this.selected + 1;
newIndex > this.results.length - 1 && (newIndex = 0),
this.selected = newIndex
},
};
};
</script>
<body
x-data={"docSearch(#{PetalPro.DocsHelpers.nav_items_json()})"}
@keydown.window.prevent.ctrl.k="openDocs()"
@keydown.window.prevent.cmd.k="openDocs()"
class="relative w-full h-full antialiased text-slate-500 dark:text-slate-400 dark:bg-slate-900"
>
<%= @inner_content %>
<!-- DOCS SEARCH -->
<!-- Modal backdrop -->
<div
class="fixed inset-0 z-50 transition-opacity bg-gray-900 backdrop-blur dark:bg-gray-800/70 bg-opacity-30"
x-show="searchOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-out duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
aria-hidden="true"
x-cloak
>
</div>
<!-- Modal dialog -->
<div
id="search-modal"
class="fixed inset-0 z-50 flex items-start justify-center px-4 mb-4 overflow-hidden transform top-20 sm:px-6"
role="dialog"
aria-modal="true"
x-show="searchOpen"
x-transition:enter="transition ease-in-out duration-200"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in-out duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
x-cloak
>
<div
class="w-full max-w-2xl max-h-full overflow-auto bg-white rounded shadow-lg scrollbar dark:bg-gray-900"
@click.outside="closeDocs()"
@keydown.escape.window="closeDocs()"
>
<!-- Search form -->
<form
@submit.prevent="selectOption()"
class="border-b border-gray-200 dark:border-gray-400"
>
<div class="relative">
<label for="modal-search" class="sr-only">
Search
</label>
<input
x-ref="searchInput"
x-model="filter"
id="modal-search"
class="w-full py-3 pl-10 pr-4 placeholder-gray-400 border-0 appearance-none dark:placeholder-gray-300 hover:dark:bg-gray-800 dark:bg-gray-900 focus:ring-transparent"
type="search"
x-on:keydown.arrow-up.prevent="focusPrevOption()"
x-on:keydown.arrow-down.prevent="focusNextOption()"
placeholder="Search Anything…"
autocomplete="off"
/>
<button class="absolute inset-0 right-auto group" type="submit" aria-label="Search">
<svg
class="flex-shrink-0 w-4 h-4 ml-4 mr-2 text-gray-400 fill-current dark:text-gray-300 group-hover:text-gray-500"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7 14c-3.86 0-7-3.14-7-7s3.14-7 7-7 7 3.14 7 7-3.14 7-7 7zM7 2C4.243 2 2 4.243 2 7s2.243 5 5 5 5-2.243 5-5-2.243-5-5-5z" />
<path d="M15.707 14.293L13.314 11.9a8.019 8.019 0 01-1.414 1.414l2.393 2.393a.997.997 0 001.414 0 .999.999 0 000-1.414z" />
</svg>
</button>
</div>
</form>
<div class="px-2 py-4">
<div class="mb-3 last:mb-0">
<div class="px-2 mb-2 text-xs font-semibold text-gray-400 uppercase">
Components
</div>
<ul class="text-sm">
<template x-for="(option, index) in results" :key="index">
<li @click="onOptionClick(index)" :class="classOption(option.path, index)">
<a
class="flex items-center p-2 rounded dark:rounded-none hover:dark:bg-gray-800"
data-phx-link="patch"
data-phx-link-state="push"
:href="option.path"
>
<div class="">
<div
x-text="option.category"
class="font-semibold text-gray-800 dark:text-gray-500 group:hover:text-white"
>
</div>
<div x-text="option.label" class="text-sm text-gray-600 dark:text-gray-300">
</div>
</div>
</a>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment