Last active
July 25, 2022 05:09
-
-
Save mplatts/038d1bea5cdd33aea956ef9103c35168 to your computer and use it in GitHub Desktop.
Combo box HEEX + Alpine JS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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