├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── autocomplete_testbed ├── .formatter.exs ├── .gitignore ├── README.md ├── assets │ ├── css │ │ └── app.css │ ├── js │ │ └── app.js │ ├── package-lock.json │ ├── package.json │ ├── tailwind.config.js │ └── vendor │ │ └── topbar.js ├── config │ ├── config.exs │ ├── dev.exs │ ├── prod.exs │ ├── runtime.exs │ └── test.exs ├── lib │ ├── autocomplete_testbed.ex │ ├── autocomplete_testbed │ │ └── application.ex │ ├── autocomplete_testbed_web.ex │ └── autocomplete_testbed_web │ │ ├── components │ │ ├── core_components.ex │ │ ├── layouts.ex │ │ └── layouts │ │ │ ├── app.html.heex │ │ │ └── root.html.heex │ │ ├── controllers │ │ ├── error_html.ex │ │ ├── error_json.ex │ │ ├── page_controller.ex │ │ ├── page_html.ex │ │ └── page_html │ │ │ └── home.html.heex │ │ ├── endpoint.ex │ │ ├── gettext.ex │ │ ├── live │ │ ├── autocompleted_form.ex │ │ └── autocompleted_form.html.heex │ │ ├── router.ex │ │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv │ ├── gettext │ │ ├── en │ │ │ └── LC_MESSAGES │ │ │ │ └── errors.po │ │ └── errors.pot │ └── static │ │ ├── favicon.ico │ │ ├── images │ │ └── logo.svg │ │ └── robots.txt └── test │ ├── autocomplete_testbed_web │ ├── controllers │ │ ├── error_html_test.exs │ │ ├── error_json_test.exs │ │ └── page_controller_test.exs │ └── features │ │ └── autocomplete_test.exs │ ├── support │ └── conn_case.ex │ └── test_helper.exs ├── lib └── autocomplete_input.ex ├── mix.exs ├── mix.lock └── test ├── autocomplete_input_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Integration Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Build and run tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout project 11 | uses: actions/checkout@v3 12 | - name: Set up Elixir 13 | uses: erlef/setup-beam@v1 14 | with: 15 | otp-version: '26.2.1' # Define the OTP version [required] 16 | elixir-version: '1.17.2-otp-26' # Define the elixir version [required] 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Install dependencies 20 | run: mix deps.get 21 | - name: Install autocomplete_testbed dependencies 22 | run: mix deps.get 23 | working-directory: ./autocomplete_testbed 24 | - name: install autocomplete_testbed assets 25 | run: npm install 26 | working-directory: autocomplete_testbed/assets 27 | - name: build autocomplete_testbed javascript 28 | run: mix assets.build 29 | working-directory: ./autocomplete_testbed 30 | - name: Run elixir tests 31 | run: mix test 32 | working-directory: ./autocomplete_testbed 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | autocomplete_input-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutocompleteInput 2 | 3 | This is an autocomplete component for Phoenix LiveView. It uses a form associated custom element, which means it appears in your form params just like any other input element in your form. It is designed to be simple to integrate with live view and fully stylable via css. 4 | 5 | ## Installation 6 | 7 | The [hex package](https://hex.pm/packages/autocomplete_input) can be installed 8 | by adding `autocomplete_input` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:autocomplete_input, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | To install the `autocomplete-input` element javascript: 19 | ``` 20 | npm install --prefix assets phoenix-custom-event-hook @launchscout/autocomplete-input 21 | ``` 22 | 23 | Add following to app.js 24 | 25 | ```js 26 | import '@launchscout/autocomplete-input' 27 | import PhoenixCustomEventHook from 'phoenix-custom-event-hook' 28 | 29 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 30 | let liveSocket = new LiveSocket("/live", Socket, { 31 | longPollFallbackMs: 2500, 32 | params: {_csrf_token: csrfToken}, 33 | hooks: { PhoenixCustomEventHook } 34 | }) 35 | ``` 36 | ## Usage 37 | 38 | The `AutocompleteInput.autocomplete_input` is a standard functional component that accepts the following attributes: 39 | 40 | * `id` - (Required) The unique identifier for the input element 41 | * `name` - (Required) The form field name that will be used in params 42 | * `options` - (Required) A list of options in the same form as `options_for_select`, eg `{label, value}` 43 | * `value` - (Optional) The initially selected value 44 | * `display_value` - (Optional) Text shown when the element renders in the intial closed state. An edit icon will be displayed next to it 45 | * `min_length` - (Optional, default: 1) Minimum number of characters required before showing suggestions 46 | 47 | ## Events 48 | 49 | The component sends two custom events that you can handle in your LiveView: 50 | 51 | * `autocomplete-search` - Triggered when the user types in the input field. The event payload contains: 52 | * `name` - The name of the field 53 | * `query` - The current search text entered by the user 54 | 55 | * `autocomplete-commit` - Triggered when an option is selected. It is normally expected you would clear the list of options here. The event payload will contain the following: 56 | * `name` The name of the field 57 | * `value` The selected value 58 | 59 | * `autocomplete-open` - Triggered when an autocomplete is opened. The event payload will contain 60 | * `name` The name of the field 61 | 62 | * `autocomplete-close` - Triggered when an autocomplete is cloase. It is normally expected that you will clear the list of options here. The event payload will contain 63 | * `name` The name of the field 64 | 65 | ## Example 66 | 67 | The component can be used within a form as follows: 68 | 69 | ```html 70 |

Autocompleted Form

71 | 72 | Selected fruit:
<%= @results[:fruit] %>
73 | Selected animal:
<%= @results[:animal] %>
74 | 75 | <.simple_form for={%{}} phx-submit="submit" phx-change="change"> 76 |
77 | 78 | <.autocomplete_input id="autocomplete-input" options={@fruit_options} value="apple" name="fruit" display_value="Choose a fruit" min_length={3} /> 79 |
80 |
81 | 82 | <.autocomplete_input id="autocomplete-input" options={@animal_options} value="dog" name="animal" display_value="Choose an animal" min_length={3} /> 83 |
84 | <.button type="submit">Submit 85 | 86 | ``` 87 | 88 | The corresponding LiveView: 89 | 90 | ```elixir 91 | defmodule AutocompleteTestbedWeb.AutocompletedForm do 92 | use AutocompleteTestbedWeb, :live_view 93 | 94 | @fruit_options [{"Apple", "apple"}, {"Banana", "banana"}, {"Cherry", "cherry"}, {"Nanna", "nanna"}] 95 | @animal_options [{"Dog", "dog"}, {"Cat", "cat"}, {"Bird", "bird"}, {"Bearcat", "bearcat"}] 96 | 97 | import AutocompleteInput 98 | 99 | def mount(params, session, socket) do 100 | {:ok, socket |> assign(fruit_options: [], animal_options: [], results: %{})} 101 | end 102 | 103 | def handle_event("autocomplete-search", %{"name" => "fruit", "query" => value}, socket) do 104 | {:noreply, socket |> assign(fruit_options: @fruit_options |> Enum.filter(&label_matches?(value, &1)))} 105 | end 106 | 107 | def handle_event("autocomplete-search", %{"name" => "animal", "query" => value}, socket) do 108 | {:noreply, socket |> assign(animal_options: @animal_options |> Enum.filter(&label_matches?(value, &1)))} 109 | end 110 | 111 | def handle_event("autocomplete-commit", _params, socket) do 112 | {:noreply, socket |> assign(options: [])} 113 | end 114 | 115 | def handle_event("change", %{"autocomplete-input" => value}, socket) do 116 | IO.inspect(value, label: "change") 117 | {:noreply, socket |> assign(selected_value: value)} 118 | end 119 | 120 | def handle_event("submit", %{"fruit" => fruit, "animal" => animal}, socket) do 121 | {:noreply, socket |> assign(results: %{fruit: fruit, animal: animal})} 122 | end 123 | 124 | defp label_matches?(query, {label, _}) do 125 | String.contains?(String.downcase(label), String.downcase(query)) 126 | end 127 | end 128 | ``` 129 | 130 | This example is contained in the `autocomplete_testbed` folder and is used by the wallaby test. 131 | 132 | Here's what it looks like in action: 133 | 134 | [![Autcomplete Demo](https://cdn.loom.com/sessions/thumbnails/1cf163f4fe544dbbae19c1660d19b9a6-a9bcaebe5225e9a9-full-play.gif)](https://www.loom.com/share/1cf163f4fe544dbbae19c1660d19b9a6?sid=c2d44f17-a0f9-4b5c-9771-da9b45b53edd) 135 | 136 | ## `autocomplete-input` 137 | 138 | For more details on the custom element this component wraps including styling information, see the [autocomplete-input repo](https://github.com/launchscout/autocomplete-input). 139 | 140 | -------------------------------------------------------------------------------- /autocomplete_testbed/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | plugins: [Phoenix.LiveView.HTMLFormatter], 4 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /autocomplete_testbed/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | autocomplete_testbed-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /autocomplete_testbed/README.md: -------------------------------------------------------------------------------- 1 | # AutocompleteTestbed 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Learn more 13 | 14 | * Official website: https://www.phoenixframework.org/ 15 | * Guides: https://hexdocs.pm/phoenix/overview.html 16 | * Docs: https://hexdocs.pm/phoenix 17 | * Forum: https://elixirforum.com/c/phoenix-forum 18 | * Source: https://github.com/phoenixframework/phoenix 19 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | /* This file is for your main application CSS */ 6 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | import PhoenixCustomEventHook from 'phoenix-custom-event-hook' 25 | import '@launchscout/autocomplete-input' 26 | 27 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 28 | let liveSocket = new LiveSocket("/live", Socket, { 29 | longPollFallbackMs: 2500, 30 | params: {_csrf_token: csrfToken}, 31 | hooks: { PhoenixCustomEventHook } 32 | }) 33 | 34 | // Show progress bar on live navigation and form submits 35 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 36 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 37 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 38 | 39 | // connect if there are any LiveViews on the page 40 | liveSocket.connect() 41 | 42 | // expose liveSocket on window for web console debug logs and latency simulation: 43 | // >> liveSocket.enableDebug() 44 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 45 | // >> liveSocket.disableLatencySim() 46 | window.liveSocket = liveSocket 47 | 48 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assets", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "@launchscout/autocomplete-input": "^0.3.2", 9 | "phoenix-custom-event-hook": "^0.0.6" 10 | } 11 | }, 12 | "node_modules/@github/combobox-nav": { 13 | "version": "3.0.1", 14 | "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-3.0.1.tgz", 15 | "integrity": "sha512-JEAm5qU+u/dU8H+NsPisJ5c/P+lfqIk4x0h+BvqBIjP7oVZq1ChPVRYG5uPmHu5Mq7HGD8IsASbpUhX5UIfRxQ==" 16 | }, 17 | "node_modules/@launchscout/autocomplete-input": { 18 | "version": "0.3.2", 19 | "resolved": "https://registry.npmjs.org/@launchscout/autocomplete-input/-/autocomplete-input-0.3.2.tgz", 20 | "integrity": "sha512-Nl+70h4OTKiD6NRh+3g3X15Fj1ZGmUJ9ELZTUpxR1NOvGslGRfUlaLtFuFZLcl1D+sq8Nfm0nEfutwes1j7XZQ==", 21 | "dependencies": { 22 | "@github/combobox-nav": "^3.0.1", 23 | "lit": "^3.1.4" 24 | } 25 | }, 26 | "node_modules/@lit-labs/ssr-dom-shim": { 27 | "version": "1.3.0", 28 | "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", 29 | "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==" 30 | }, 31 | "node_modules/@lit/reactive-element": { 32 | "version": "2.0.4", 33 | "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", 34 | "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", 35 | "dependencies": { 36 | "@lit-labs/ssr-dom-shim": "^1.2.0" 37 | } 38 | }, 39 | "node_modules/@types/trusted-types": { 40 | "version": "2.0.7", 41 | "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 42 | "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" 43 | }, 44 | "node_modules/lit": { 45 | "version": "3.2.1", 46 | "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", 47 | "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", 48 | "dependencies": { 49 | "@lit/reactive-element": "^2.0.4", 50 | "lit-element": "^4.1.0", 51 | "lit-html": "^3.2.0" 52 | } 53 | }, 54 | "node_modules/lit-element": { 55 | "version": "4.1.1", 56 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", 57 | "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", 58 | "dependencies": { 59 | "@lit-labs/ssr-dom-shim": "^1.2.0", 60 | "@lit/reactive-element": "^2.0.4", 61 | "lit-html": "^3.2.0" 62 | } 63 | }, 64 | "node_modules/lit-html": { 65 | "version": "3.2.1", 66 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", 67 | "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", 68 | "dependencies": { 69 | "@types/trusted-types": "^2.0.2" 70 | } 71 | }, 72 | "node_modules/ms": { 73 | "version": "2.1.3", 74 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 75 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 76 | }, 77 | "node_modules/phoenix-custom-event-hook": { 78 | "version": "0.0.6", 79 | "resolved": "https://registry.npmjs.org/phoenix-custom-event-hook/-/phoenix-custom-event-hook-0.0.6.tgz", 80 | "integrity": "sha512-6rPab4rL3UwhV1En4ACvwbyG7Y3o0/I2yJDpI6BgHdt8ie/LaMG+OL3MjYt67kGybWYGC/EhyXdUsZGng3dqtA==", 81 | "dependencies": { 82 | "ms": "^2.0.0" 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@launchscout/autocomplete-input": "^0.3.3", 4 | "phoenix-custom-event-hook": "^0.0.6" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | const fs = require("fs") 6 | const path = require("path") 7 | 8 | module.exports = { 9 | content: [ 10 | "./js/**/*.js", 11 | "../lib/autocomplete_testbed_web.ex", 12 | "../lib/autocomplete_testbed_web/**/*.*ex" 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | brand: "#FD4F00", 18 | } 19 | }, 20 | }, 21 | plugins: [ 22 | require("@tailwindcss/forms"), 23 | // Allows prefixing tailwind classes with LiveView classes to add rules 24 | // only when LiveView classes are applied, for example: 25 | // 26 | //
27 | // 28 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 29 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 30 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), 31 | 32 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle 33 | // See your `CoreComponents.icon/1` for more information. 34 | // 35 | plugin(function({matchComponents, theme}) { 36 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") 37 | let values = {} 38 | let icons = [ 39 | ["", "/24/outline"], 40 | ["-solid", "/24/solid"], 41 | ["-mini", "/20/solid"], 42 | ["-micro", "/16/solid"] 43 | ] 44 | icons.forEach(([suffix, dir]) => { 45 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { 46 | let name = path.basename(file, ".svg") + suffix 47 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)} 48 | }) 49 | }) 50 | matchComponents({ 51 | "hero": ({name, fullPath}) => { 52 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") 53 | let size = theme("spacing.6") 54 | if (name.endsWith("-mini")) { 55 | size = theme("spacing.5") 56 | } else if (name.endsWith("-micro")) { 57 | size = theme("spacing.4") 58 | } 59 | return { 60 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, 61 | "-webkit-mask": `var(--hero-${name})`, 62 | "mask": `var(--hero-${name})`, 63 | "mask-repeat": "no-repeat", 64 | "background-color": "currentColor", 65 | "vertical-align": "middle", 66 | "display": "inline-block", 67 | "width": size, 68 | "height": size 69 | } 70 | } 71 | }, {values}) 72 | }) 73 | ] 74 | } 75 | -------------------------------------------------------------------------------- /autocomplete_testbed/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /autocomplete_testbed/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :autocomplete_testbed, 11 | generators: [timestamp_type: :utc_datetime] 12 | 13 | # Configures the endpoint 14 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 15 | url: [host: "localhost"], 16 | adapter: Bandit.PhoenixAdapter, 17 | render_errors: [ 18 | formats: [html: AutocompleteTestbedWeb.ErrorHTML, json: AutocompleteTestbedWeb.ErrorJSON], 19 | layout: false 20 | ], 21 | pubsub_server: AutocompleteTestbed.PubSub, 22 | live_view: [signing_salt: "Dz/OG7r8"] 23 | 24 | # Configure esbuild (the version is required) 25 | config :esbuild, 26 | version: "0.17.11", 27 | autocomplete_testbed: [ 28 | args: 29 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 30 | cd: Path.expand("../assets", __DIR__), 31 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 32 | ] 33 | 34 | # Configure tailwind (the version is required) 35 | config :tailwind, 36 | version: "3.4.3", 37 | autocomplete_testbed: [ 38 | args: ~w( 39 | --config=tailwind.config.js 40 | --input=css/app.css 41 | --output=../priv/static/assets/app.css 42 | ), 43 | cd: Path.expand("../assets", __DIR__) 44 | ] 45 | 46 | # Configures Elixir's Logger 47 | config :logger, :console, 48 | format: "$time $metadata[$level] $message\n", 49 | metadata: [:request_id] 50 | 51 | # Use Jason for JSON parsing in Phoenix 52 | config :phoenix, :json_library, Jason 53 | 54 | # Import environment specific config. This must remain at the bottom 55 | # of this file so it overrides the configuration defined above. 56 | import_config "#{config_env()}.exs" 57 | -------------------------------------------------------------------------------- /autocomplete_testbed/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # For development, we disable any cache and enable 4 | # debugging and code reloading. 5 | # 6 | # The watchers configuration can be used to run external 7 | # watchers to your application. For example, we can use it 8 | # to bundle .js and .css sources. 9 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 10 | # Binding to loopback ipv4 address prevents access from other machines. 11 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 12 | http: [ip: {127, 0, 0, 1}, port: 4000], 13 | check_origin: false, 14 | code_reloader: true, 15 | debug_errors: true, 16 | secret_key_base: "Ko4SVYq6UM/Plte+gnAUz6uQr0c4vat4wyRxJJjG+UV0L3SinzMsNbkKz2pgs6bI", 17 | watchers: [ 18 | esbuild: {Esbuild, :install_and_run, [:autocomplete_testbed, ~w(--sourcemap=inline --watch)]}, 19 | tailwind: {Tailwind, :install_and_run, [:autocomplete_testbed, ~w(--watch)]} 20 | ] 21 | 22 | # ## SSL Support 23 | # 24 | # In order to use HTTPS in development, a self-signed 25 | # certificate can be generated by running the following 26 | # Mix task: 27 | # 28 | # mix phx.gen.cert 29 | # 30 | # Run `mix help phx.gen.cert` for more information. 31 | # 32 | # The `http:` config above can be replaced with: 33 | # 34 | # https: [ 35 | # port: 4001, 36 | # cipher_suite: :strong, 37 | # keyfile: "priv/cert/selfsigned_key.pem", 38 | # certfile: "priv/cert/selfsigned.pem" 39 | # ], 40 | # 41 | # If desired, both `http:` and `https:` keys can be 42 | # configured to run both http and https servers on 43 | # different ports. 44 | 45 | # Watch static and templates for browser reloading. 46 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 47 | live_reload: [ 48 | patterns: [ 49 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 50 | ~r"priv/gettext/.*(po)$", 51 | ~r"lib/autocomplete_testbed_web/(controllers|live|components)/.*(ex|heex)$" 52 | ] 53 | ] 54 | 55 | # Enable dev routes for dashboard and mailbox 56 | config :autocomplete_testbed, dev_routes: true 57 | 58 | # Do not include metadata nor timestamps in development logs 59 | config :logger, :console, format: "[$level] $message\n" 60 | 61 | # Set a higher stacktrace during development. Avoid configuring such 62 | # in production as building large stacktraces may be expensive. 63 | config :phoenix, :stacktrace_depth, 20 64 | 65 | # Initialize plugs at runtime for faster development compilation 66 | config :phoenix, :plug_init_mode, :runtime 67 | 68 | config :phoenix_live_view, 69 | # Include HEEx debug annotations as HTML comments in rendered markup 70 | debug_heex_annotations: true, 71 | # Enable helpful, but potentially expensive runtime checks 72 | enable_expensive_runtime_checks: true 73 | -------------------------------------------------------------------------------- /autocomplete_testbed/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 9 | cache_static_manifest: "priv/static/cache_manifest.json" 10 | 11 | # Do not print debug messages in production 12 | config :logger, level: :info 13 | 14 | # Runtime production configuration, including reading 15 | # of environment variables, is done on config/runtime.exs. 16 | -------------------------------------------------------------------------------- /autocomplete_testbed/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/autocomplete_testbed start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | # The secret key base is used to sign/encrypt cookies and other secrets. 25 | # A default value is used in config/dev.exs and config/test.exs but you 26 | # want to use a different value for prod and you most likely don't want 27 | # to check this value into version control, so we use an environment 28 | # variable instead. 29 | secret_key_base = 30 | System.get_env("SECRET_KEY_BASE") || 31 | raise """ 32 | environment variable SECRET_KEY_BASE is missing. 33 | You can generate one by calling: mix phx.gen.secret 34 | """ 35 | 36 | host = System.get_env("PHX_HOST") || "example.com" 37 | port = String.to_integer(System.get_env("PORT") || "4000") 38 | 39 | config :autocomplete_testbed, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 40 | 41 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 42 | url: [host: host, port: 443, scheme: "https"], 43 | http: [ 44 | # Enable IPv6 and bind on all interfaces. 45 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 46 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 47 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 48 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 49 | port: port 50 | ], 51 | secret_key_base: secret_key_base 52 | 53 | # ## SSL Support 54 | # 55 | # To get SSL working, you will need to add the `https` key 56 | # to your endpoint configuration: 57 | # 58 | # config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 59 | # https: [ 60 | # ..., 61 | # port: 443, 62 | # cipher_suite: :strong, 63 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 64 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 65 | # ] 66 | # 67 | # The `cipher_suite` is set to `:strong` to support only the 68 | # latest and more secure SSL ciphers. This means old browsers 69 | # and clients may not be supported. You can set it to 70 | # `:compatible` for wider support. 71 | # 72 | # `:keyfile` and `:certfile` expect an absolute path to the key 73 | # and cert in disk or a relative path inside priv, for example 74 | # "priv/ssl/server.key". For all supported SSL configuration 75 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 76 | # 77 | # We also recommend setting `force_ssl` in your config/prod.exs, 78 | # ensuring no data is ever sent via http, always redirecting to https: 79 | # 80 | # config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 81 | # force_ssl: [hsts: true] 82 | # 83 | # Check `Plug.SSL` for all available options in `force_ssl`. 84 | end 85 | -------------------------------------------------------------------------------- /autocomplete_testbed/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :autocomplete_testbed, AutocompleteTestbedWeb.Endpoint, 6 | http: [ip: {127, 0, 0, 1}, port: 4002], 7 | secret_key_base: "M0aTg700+CNTDNaf8dfCu+jT35DO44NxOQ5HxO+IeBQNsVmDXgWzaUJfgNNPiIsP", 8 | server: true 9 | 10 | # Print only warnings and errors during test 11 | config :logger, level: :warning 12 | 13 | # Initialize plugs at runtime for faster test compilation 14 | config :phoenix, :plug_init_mode, :runtime 15 | 16 | # Enable helpful, but potentially expensive runtime checks 17 | config :phoenix_live_view, 18 | enable_expensive_runtime_checks: true 19 | 20 | config :wallaby, 21 | otp_app: :autocomplete_testbed, 22 | base_url: "http://localhost:4002" 23 | # chromedriver: [ 24 | # headless: false 25 | # ] 26 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbed do 2 | @moduledoc """ 3 | AutocompleteTestbed keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed/application.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbed.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | AutocompleteTestbedWeb.Telemetry, 12 | {DNSCluster, query: Application.get_env(:autocomplete_testbed, :dns_cluster_query) || :ignore}, 13 | {Phoenix.PubSub, name: AutocompleteTestbed.PubSub}, 14 | # Start a worker by calling: AutocompleteTestbed.Worker.start_link(arg) 15 | # {AutocompleteTestbed.Worker, arg}, 16 | # Start to serve requests, typically the last entry 17 | AutocompleteTestbedWeb.Endpoint 18 | ] 19 | 20 | # See https://hexdocs.pm/elixir/Supervisor.html 21 | # for other strategies and supported options 22 | opts = [strategy: :one_for_one, name: AutocompleteTestbed.Supervisor] 23 | Supervisor.start_link(children, opts) 24 | end 25 | 26 | # Tell Phoenix to update the endpoint configuration 27 | # whenever the application is updated. 28 | @impl true 29 | def config_change(changed, _new, removed) do 30 | AutocompleteTestbedWeb.Endpoint.config_change(changed, removed) 31 | :ok 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use AutocompleteTestbedWeb, :controller 9 | use AutocompleteTestbedWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: AutocompleteTestbedWeb.Layouts] 44 | 45 | use Gettext, backend: AutocompleteTestbedWeb.Gettext 46 | 47 | import Plug.Conn 48 | 49 | unquote(verified_routes()) 50 | end 51 | end 52 | 53 | def live_view do 54 | quote do 55 | use Phoenix.LiveView, 56 | layout: {AutocompleteTestbedWeb.Layouts, :app} 57 | 58 | unquote(html_helpers()) 59 | end 60 | end 61 | 62 | def live_component do 63 | quote do 64 | use Phoenix.LiveComponent 65 | 66 | unquote(html_helpers()) 67 | end 68 | end 69 | 70 | def html do 71 | quote do 72 | use Phoenix.Component 73 | 74 | # Import convenience functions from controllers 75 | import Phoenix.Controller, 76 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 77 | 78 | # Include general helpers for rendering HTML 79 | unquote(html_helpers()) 80 | end 81 | end 82 | 83 | defp html_helpers do 84 | quote do 85 | # Translation 86 | use Gettext, backend: AutocompleteTestbedWeb.Gettext 87 | 88 | # HTML escaping functionality 89 | import Phoenix.HTML 90 | # Core UI components 91 | import AutocompleteTestbedWeb.CoreComponents 92 | 93 | # Shortcut for generating JS commands 94 | alias Phoenix.LiveView.JS 95 | 96 | # Routes generation with the ~p sigil 97 | unquote(verified_routes()) 98 | end 99 | end 100 | 101 | def verified_routes do 102 | quote do 103 | use Phoenix.VerifiedRoutes, 104 | endpoint: AutocompleteTestbedWeb.Endpoint, 105 | router: AutocompleteTestbedWeb.Router, 106 | statics: AutocompleteTestbedWeb.static_paths() 107 | end 108 | end 109 | 110 | @doc """ 111 | When used, dispatch to the appropriate controller/live_view/etc. 112 | """ 113 | defmacro __using__(which) when is_atom(which) do 114 | apply(__MODULE__, which, []) 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/components/core_components.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.CoreComponents do 2 | @moduledoc """ 3 | Provides core UI components. 4 | 5 | At first glance, this module may seem daunting, but its goal is to provide 6 | core building blocks for your application, such as modals, tables, and 7 | forms. The components consist mostly of markup and are well-documented 8 | with doc strings and declarative assigns. You may customize and style 9 | them in any way you want, based on your application growth and needs. 10 | 11 | The default components use Tailwind CSS, a utility-first CSS framework. 12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn 13 | how to customize them or feel free to swap in another framework altogether. 14 | 15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. 16 | """ 17 | use Phoenix.Component 18 | use Gettext, backend: AutocompleteTestbedWeb.Gettext 19 | 20 | alias Phoenix.LiveView.JS 21 | 22 | @doc """ 23 | Renders a modal. 24 | 25 | ## Examples 26 | 27 | <.modal id="confirm-modal"> 28 | This is a modal. 29 | 30 | 31 | JS commands may be passed to the `:on_cancel` to configure 32 | the closing/cancel event, for example: 33 | 34 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> 35 | This is another modal. 36 | 37 | 38 | """ 39 | attr :id, :string, required: true 40 | attr :show, :boolean, default: false 41 | attr :on_cancel, JS, default: %JS{} 42 | slot :inner_block, required: true 43 | 44 | def modal(assigns) do 45 | ~H""" 46 | 328 | """ 329 | end 330 | 331 | def input(%{type: "select"} = assigns) do 332 | ~H""" 333 |
334 | <.label for={@id}>{@label} 335 | 345 | <.error :for={msg <- @errors}>{msg} 346 |
347 | """ 348 | end 349 | 350 | def input(%{type: "textarea"} = assigns) do 351 | ~H""" 352 |
353 | <.label for={@id}>{@label} 354 | 364 | <.error :for={msg <- @errors}>{msg} 365 |
366 | """ 367 | end 368 | 369 | # All other inputs text, datetime-local, url, password, etc. are handled here... 370 | def input(assigns) do 371 | ~H""" 372 |
373 | <.label for={@id}>{@label} 374 | 386 | <.error :for={msg <- @errors}>{msg} 387 |
388 | """ 389 | end 390 | 391 | @doc """ 392 | Renders a label. 393 | """ 394 | attr :for, :string, default: nil 395 | slot :inner_block, required: true 396 | 397 | def label(assigns) do 398 | ~H""" 399 | 402 | """ 403 | end 404 | 405 | @doc """ 406 | Generates a generic error message. 407 | """ 408 | slot :inner_block, required: true 409 | 410 | def error(assigns) do 411 | ~H""" 412 |

413 | <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> 414 | {render_slot(@inner_block)} 415 |

416 | """ 417 | end 418 | 419 | @doc """ 420 | Renders a header with title. 421 | """ 422 | attr :class, :string, default: nil 423 | 424 | slot :inner_block, required: true 425 | slot :subtitle 426 | slot :actions 427 | 428 | def header(assigns) do 429 | ~H""" 430 |
431 |
432 |

433 | {render_slot(@inner_block)} 434 |

435 |

436 | {render_slot(@subtitle)} 437 |

438 |
439 |
{render_slot(@actions)}
440 |
441 | """ 442 | end 443 | 444 | @doc ~S""" 445 | Renders a table with generic styling. 446 | 447 | ## Examples 448 | 449 | <.table id="users" rows={@users}> 450 | <:col :let={user} label="id">{user.id} 451 | <:col :let={user} label="username">{user.username} 452 | 453 | """ 454 | attr :id, :string, required: true 455 | attr :rows, :list, required: true 456 | attr :row_id, :any, default: nil, doc: "the function for generating the row id" 457 | attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" 458 | 459 | attr :row_item, :any, 460 | default: &Function.identity/1, 461 | doc: "the function for mapping each row before calling the :col and :action slots" 462 | 463 | slot :col, required: true do 464 | attr :label, :string 465 | end 466 | 467 | slot :action, doc: "the slot for showing user actions in the last table column" 468 | 469 | def table(assigns) do 470 | assigns = 471 | with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do 472 | assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) 473 | end 474 | 475 | ~H""" 476 |
477 | 478 | 479 | 480 | 481 | 484 | 485 | 486 | 491 | 492 | 504 | 515 | 516 | 517 |
{col[:label]} 482 | {gettext("Actions")} 483 |
497 |
498 | 499 | 500 | {render_slot(col, @row_item.(row))} 501 | 502 |
503 |
505 |
506 | 507 | 511 | {render_slot(action, @row_item.(row))} 512 | 513 |
514 |
518 |
519 | """ 520 | end 521 | 522 | @doc """ 523 | Renders a data list. 524 | 525 | ## Examples 526 | 527 | <.list> 528 | <:item title="Title">{@post.title} 529 | <:item title="Views">{@post.views} 530 | 531 | """ 532 | slot :item, required: true do 533 | attr :title, :string, required: true 534 | end 535 | 536 | def list(assigns) do 537 | ~H""" 538 |
539 |
540 |
541 |
{item.title}
542 |
{render_slot(item)}
543 |
544 |
545 |
546 | """ 547 | end 548 | 549 | @doc """ 550 | Renders a back navigation link. 551 | 552 | ## Examples 553 | 554 | <.back navigate={~p"/posts"}>Back to posts 555 | """ 556 | attr :navigate, :any, required: true 557 | slot :inner_block, required: true 558 | 559 | def back(assigns) do 560 | ~H""" 561 |
562 | <.link 563 | navigate={@navigate} 564 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" 565 | > 566 | <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> 567 | {render_slot(@inner_block)} 568 | 569 |
570 | """ 571 | end 572 | 573 | @doc """ 574 | Renders a [Heroicon](https://heroicons.com). 575 | 576 | Heroicons come in three styles – outline, solid, and mini. 577 | By default, the outline style is used, but solid and mini may 578 | be applied by using the `-solid` and `-mini` suffix. 579 | 580 | You can customize the size and colors of the icons by setting 581 | width, height, and background color classes. 582 | 583 | Icons are extracted from the `deps/heroicons` directory and bundled within 584 | your compiled app.css by the plugin in your `assets/tailwind.config.js`. 585 | 586 | ## Examples 587 | 588 | <.icon name="hero-x-mark-solid" /> 589 | <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> 590 | """ 591 | attr :name, :string, required: true 592 | attr :class, :string, default: nil 593 | 594 | def icon(%{name: "hero-" <> _} = assigns) do 595 | ~H""" 596 | 597 | """ 598 | end 599 | 600 | ## JS Commands 601 | 602 | def show(js \\ %JS{}, selector) do 603 | JS.show(js, 604 | to: selector, 605 | time: 300, 606 | transition: 607 | {"transition-all transform ease-out duration-300", 608 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", 609 | "opacity-100 translate-y-0 sm:scale-100"} 610 | ) 611 | end 612 | 613 | def hide(js \\ %JS{}, selector) do 614 | JS.hide(js, 615 | to: selector, 616 | time: 200, 617 | transition: 618 | {"transition-all transform ease-in duration-200", 619 | "opacity-100 translate-y-0 sm:scale-100", 620 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} 621 | ) 622 | end 623 | 624 | def show_modal(js \\ %JS{}, id) when is_binary(id) do 625 | js 626 | |> JS.show(to: "##{id}") 627 | |> JS.show( 628 | to: "##{id}-bg", 629 | time: 300, 630 | transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} 631 | ) 632 | |> show("##{id}-container") 633 | |> JS.add_class("overflow-hidden", to: "body") 634 | |> JS.focus_first(to: "##{id}-content") 635 | end 636 | 637 | def hide_modal(js \\ %JS{}, id) do 638 | js 639 | |> JS.hide( 640 | to: "##{id}-bg", 641 | transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} 642 | ) 643 | |> hide("##{id}-container") 644 | |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) 645 | |> JS.remove_class("overflow-hidden", to: "body") 646 | |> JS.pop_focus() 647 | end 648 | 649 | @doc """ 650 | Translates an error message using gettext. 651 | """ 652 | def translate_error({msg, opts}) do 653 | # When using gettext, we typically pass the strings we want 654 | # to translate as a static argument: 655 | # 656 | # # Translate the number of files with plural rules 657 | # dngettext("errors", "1 file", "%{count} files", count) 658 | # 659 | # However the error messages in our forms and APIs are generated 660 | # dynamically, so we need to translate them by calling Gettext 661 | # with our gettext backend as first argument. Translations are 662 | # available in the errors.po file (as we use the "errors" domain). 663 | if count = opts[:count] do 664 | Gettext.dngettext(AutocompleteTestbedWeb.Gettext, "errors", msg, msg, count, opts) 665 | else 666 | Gettext.dgettext(AutocompleteTestbedWeb.Gettext, "errors", msg, opts) 667 | end 668 | end 669 | 670 | @doc """ 671 | Translates the errors for a field from a keyword list of errors. 672 | """ 673 | def translate_errors(errors, field) when is_list(errors) do 674 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) 675 | end 676 | end 677 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use AutocompleteTestbedWeb, :controller` and 9 | `use AutocompleteTestbedWeb, :live_view`. 10 | """ 11 | use AutocompleteTestbedWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v{Application.spec(:phoenix, :vsn)} 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | {@inner_content} 31 |
32 |
33 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title default="AutocompleteTestbed" suffix=" · Phoenix Framework"> 8 | {assigns[:page_title]} 9 | 10 | 11 | 13 | 14 | 15 | {@inner_content} 16 | 17 | 18 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use AutocompleteTestbedWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/autocomplete_testbed_web/controllers/error_html/404.html.heex 14 | # * lib/autocomplete_testbed_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.PageController do 2 | use AutocompleteTestbedWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use AutocompleteTestbedWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/controllers/page_html/home.html.heex: -------------------------------------------------------------------------------- 1 | <.flash_group flash={@flash} /> 2 | 41 |
42 |
43 | 49 |

50 | Phoenix Framework 51 | 52 | v{Application.spec(:phoenix, :vsn)} 53 | 54 |

55 |

56 | Peace of mind from prototype to production. 57 |

58 |

59 | Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. 60 |

61 | 221 |
222 |
223 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :autocomplete_testbed 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_autocomplete_testbed_key", 10 | signing_salt: "W17WV39n", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :autocomplete_testbed, 25 | gzip: false, 26 | only: AutocompleteTestbedWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | end 35 | 36 | plug Plug.RequestId 37 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 38 | 39 | plug Plug.Parsers, 40 | parsers: [:urlencoded, :multipart, :json], 41 | pass: ["*/*"], 42 | json_decoder: Phoenix.json_library() 43 | 44 | plug Plug.MethodOverride 45 | plug Plug.Head 46 | plug Plug.Session, @session_options 47 | plug AutocompleteTestbedWeb.Router 48 | end 49 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), your module compiles translations 6 | that you can use in your application. To use this Gettext backend module, 7 | call `use Gettext` and pass it as an option: 8 | 9 | use Gettext, backend: AutocompleteTestbedWeb.Gettext 10 | 11 | # Simple translation 12 | gettext("Here is the string to translate") 13 | 14 | # Plural translation 15 | ngettext("Here is the string to translate", 16 | "Here are the strings to translate", 17 | 3) 18 | 19 | # Domain-based translation 20 | dgettext("errors", "Here is the error message to translate") 21 | 22 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 23 | """ 24 | use Gettext.Backend, otp_app: :autocomplete_testbed 25 | end 26 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/live/autocompleted_form.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.AutocompletedForm do 2 | use AutocompleteTestbedWeb, :live_view 3 | 4 | @fruit_options [{"Apple", "apple"}, {"Banana", "banana"}, {"Cherry", "cherry"}, {"Nanna", "nanna"}] 5 | @animal_options [{"Dog", "dog"}, {"Cat", "cat"}, {"Bird", "bird"}, {"Bearcat", "bearcat"}] 6 | 7 | import AutocompleteInput 8 | 9 | def mount(params, session, socket) do 10 | {:ok, socket |> assign(fruit_options: [], animal_options: [], results: %{})} 11 | end 12 | 13 | def handle_event("autocomplete-search", %{"name" => "fruit", "query" => value}, socket) do 14 | {:noreply, socket |> assign(fruit_options: @fruit_options |> Enum.filter(&label_matches?(value, &1)))} 15 | end 16 | 17 | def handle_event("autocomplete-search", %{"name" => "animal", "query" => value}, socket) do 18 | {:noreply, socket |> assign(animal_options: @animal_options |> Enum.filter(&label_matches?(value, &1)))} 19 | end 20 | 21 | def handle_event("autocomplete-commit", %{"name" => "animal"}, socket) do 22 | {:noreply, socket |> assign(animal_options: [])} 23 | end 24 | 25 | def handle_event("autocomplete-commit", %{"name" => "fruit"}, socket) do 26 | {:noreply, socket |> assign(fruit_options: [])} 27 | end 28 | 29 | def handle_event("autocomplete-close", %{"name" => "animal"}, socket) do 30 | {:noreply, socket |> assign(animal_options: [])} 31 | end 32 | 33 | def handle_event("autocomplete-close", %{"name" => "fruit"}, socket) do 34 | {:noreply, socket |> assign(fruit_options: [])} 35 | end 36 | 37 | def handle_event("autocomplete-open", _params, socket), do: {:noreply, socket} 38 | 39 | def handle_event("change", %{"autocomplete-input" => value}, socket) do 40 | IO.inspect(value, label: "change") 41 | {:noreply, socket |> assign(selected_value: value)} 42 | end 43 | 44 | def handle_event("submit", %{"fruit" => fruit, "animal" => animal}, socket) do 45 | {:noreply, socket |> assign(results: %{fruit: fruit, animal: animal})} 46 | end 47 | 48 | defp label_matches?(query, {label, _}) do 49 | String.contains?(String.downcase(label), String.downcase(query)) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/live/autocompleted_form.html.heex: -------------------------------------------------------------------------------- 1 |

Autocompleted Form

2 | 3 | Selected fruit:
<%= @results[:fruit] %>
4 | Selected animal:
<%= @results[:animal] %>
5 | 6 | <.simple_form for={%{}} phx-submit="submit" phx-change="change"> 7 |
8 | 9 | <.autocomplete_input id="autocomplete-fruit" options={@fruit_options} value="apple" name="fruit" display_value="Choose a fruit" min_length={3} /> 10 |
11 |
12 | 13 | <.autocomplete_input id="autocomplete-animal" options={@animal_options} value="dog" name="animal" display_value="Choose an animal" min_length={3} /> 14 |
15 | <.button type="submit">Submit 16 | 17 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.Router do 2 | use AutocompleteTestbedWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {AutocompleteTestbedWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", AutocompleteTestbedWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :home 21 | 22 | live "/autocompleted-form", AutocompletedForm 23 | end 24 | 25 | # Other scopes may use custom stacks. 26 | # scope "/api", AutocompleteTestbedWeb do 27 | # pipe_through :api 28 | # end 29 | end 30 | -------------------------------------------------------------------------------- /autocomplete_testbed/lib/autocomplete_testbed_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | summary("phoenix.channel_joined.duration", 47 | unit: {:native, :millisecond} 48 | ), 49 | summary("phoenix.channel_handled_in.duration", 50 | tags: [:event], 51 | unit: {:native, :millisecond} 52 | ), 53 | 54 | # VM Metrics 55 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 56 | summary("vm.total_run_queue_lengths.total"), 57 | summary("vm.total_run_queue_lengths.cpu"), 58 | summary("vm.total_run_queue_lengths.io") 59 | ] 60 | end 61 | 62 | defp periodic_measurements do 63 | [ 64 | # A module, function and arguments to be invoked periodically. 65 | # This function must call :telemetry.execute/3 and a metric must be added above. 66 | # {AutocompleteTestbedWeb, :count_users, []} 67 | ] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /autocomplete_testbed/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbed.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :autocomplete_testbed, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {AutocompleteTestbed.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.18"}, 36 | {:phoenix_html, "~> 4.1"}, 37 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 38 | {:phoenix_live_view, "~> 1.0.0"}, 39 | {:floki, ">= 0.30.0", only: :test}, 40 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 41 | {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, 42 | {:heroicons, 43 | github: "tailwindlabs/heroicons", 44 | tag: "v2.1.1", 45 | sparse: "optimized", 46 | app: false, 47 | compile: false, 48 | depth: 1}, 49 | {:telemetry_metrics, "~> 1.0"}, 50 | {:telemetry_poller, "~> 1.0"}, 51 | {:gettext, "~> 0.26"}, 52 | {:jason, "~> 1.2"}, 53 | {:dns_cluster, "~> 0.1.1"}, 54 | {:bandit, "~> 1.5"}, 55 | {:autocomplete_input, path: ".."}, 56 | {:wallaby, "~> 0.30.2", 57 | git: "https://github.com/launchscout/wallaby.git", 58 | branch: "shadow-dom", 59 | runtime: false, 60 | only: :test} 61 | ] 62 | end 63 | 64 | # Aliases are shortcuts or tasks specific to the current project. 65 | # For example, to install project dependencies and perform other setup tasks, run: 66 | # 67 | # $ mix setup 68 | # 69 | # See the documentation for `Mix` for more info on aliases. 70 | defp aliases do 71 | [ 72 | setup: ["deps.get", "assets.setup", "assets.build"], 73 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], 74 | "assets.build": ["tailwind autocomplete_testbed", "esbuild autocomplete_testbed"], 75 | "assets.deploy": [ 76 | "tailwind autocomplete_testbed --minify", 77 | "esbuild autocomplete_testbed --minify", 78 | "phx.digest" 79 | ] 80 | ] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /autocomplete_testbed/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bandit": {:hex, :bandit, "1.6.6", "f2019a95261d400579075df5bc15641ba8e446cc4777ede6b4ec19e434c3340d", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "ceb19bf154bc2c07ee0c9addf407d817c48107e36a66351500846fc325451bf9"}, 3 | "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, 4 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 5 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 7 | "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, 8 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 9 | "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, 10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 11 | "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, 12 | "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, 13 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 14 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, 15 | "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, 16 | "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, 17 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 18 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 19 | "live_elements": {:hex, :live_elements, "0.2.4", "7e67ea1a48f386fbd17c3989f85a29e4597c4e83ab30e923f6f02135ec1847bd", [:mix], [{:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "ecc3c2c1fe369785231ceca60af15d0229032ace03f00f259b4049469d1c2cc3"}, 20 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 21 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 22 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 23 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 24 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 25 | "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 27 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 28 | "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"}, 29 | "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, 30 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, 31 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.3", "33914a5d47345c7bde056054bca05c0bbbda5ce94a3ee734cb20a4d5d361e20d", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "157d93a5a7c79b836d9b7b5d55c7dd021435139d6160112f0e630f0686b8ea88"}, 32 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 33 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 34 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 35 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 36 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 37 | "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, 38 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 39 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, 40 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, 41 | "tesla": {:hex, :tesla, "1.13.2", "85afa342eb2ac0fee830cf649dbd19179b6b359bec4710d02a3d5d587f016910", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "960609848f1ef654c3cdfad68453cd84a5febecb6ed9fed9416e36cd9cd724f9"}, 42 | "thousand_island": {:hex, :thousand_island, "1.3.9", "095db3e2650819443e33237891271943fad3b7f9ba341073947581362582ab5a", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25ab4c07badadf7f87adb4ab414e0ed374e5f19e72503aa85132caa25776e54f"}, 43 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 44 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, 45 | "wallaby": {:git, "https://github.com/launchscout/wallaby.git", "158f6aa8c98b16c286a909bf86ab7def3f9ff880", [branch: "shadow-dom"]}, 46 | "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, 47 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 48 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 49 | } 50 | -------------------------------------------------------------------------------- /autocomplete_testbed/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | -------------------------------------------------------------------------------- /autocomplete_testbed/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | -------------------------------------------------------------------------------- /autocomplete_testbed/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchscout/autocomplete_input/b4780bd09d4beae94ed81cbe8ef82a1acad38a91/autocomplete_testbed/priv/static/favicon.ico -------------------------------------------------------------------------------- /autocomplete_testbed/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /autocomplete_testbed/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/autocomplete_testbed_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.ErrorHTMLTest do 2 | use AutocompleteTestbedWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(AutocompleteTestbedWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(AutocompleteTestbedWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/autocomplete_testbed_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.ErrorJSONTest do 2 | use AutocompleteTestbedWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert AutocompleteTestbedWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert AutocompleteTestbedWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/autocomplete_testbed_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.PageControllerTest do 2 | use AutocompleteTestbedWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/autocomplete_testbed_web/features/autocomplete_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.AutocompleteTest do 2 | use ExUnit.Case 3 | use Wallaby.Feature 4 | 5 | import Wallaby.Query 6 | 7 | 8 | feature "autocomplete input", %{session: session} do 9 | session 10 | |> visit("/autocompleted-form") 11 | |> find(css("autocomplete-input[name='fruit']")) 12 | |> shadow_root() 13 | |> click(css("span", text: "Choose a fruit")) 14 | |> fill_in(css("input"), with: "nan") 15 | |> assert_has(css("li", text: "Nanna")) 16 | |> assert_has(css("li", text: "Banana")) 17 | |> click(css("li", text: "Nanna")) 18 | |> assert_has(css("span", text: "Nanna")) 19 | |> refute_has(css("li", text: "Banana")) 20 | 21 | session |> click(css("button", text: "Submit")) |> assert_has(css("#selected-fruit", text: "nanna")) 22 | end 23 | 24 | feature "doing the same search twice", %{session: session} do 25 | session 26 | |> visit("/autocompleted-form") 27 | |> find(css("autocomplete-input[name='animal']")) 28 | |> shadow_root() 29 | |> click(css("span", text: "Choose an animal")) 30 | |> fill_in(css("input"), with: "cat") 31 | |> assert_has(css("li", text: "Cat")) 32 | |> click(css(".cancel-icon")) 33 | |> click(css("span", text: "Choose an animal")) 34 | |> fill_in(css("input"), with: "cat") 35 | |> assert_has(css("li", text: "Cat")) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteTestbedWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use AutocompleteTestbedWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint AutocompleteTestbedWeb.Endpoint 24 | 25 | use AutocompleteTestbedWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import AutocompleteTestbedWeb.ConnCase 31 | end 32 | end 33 | 34 | setup _tags do 35 | {:ok, conn: Phoenix.ConnTest.build_conn()} 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /autocomplete_testbed/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | {:ok, _} = Application.ensure_all_started(:wallaby) 2 | 3 | ExUnit.start() 4 | -------------------------------------------------------------------------------- /lib/autocomplete_input.ex: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteInput do 2 | use Phoenix.Component 3 | 4 | attr(:options, :list, required: true) 5 | attr(:id, :string, required: true) 6 | attr(:name, :string, required: true) 7 | attr(:display_value, :string, required: true) 8 | attr(:value, :string, required: true) 9 | attr(:min_length, :integer, required: false) 10 | 11 | def autocomplete_input(assigns) do 12 | ~H""" 13 | 23 | 24 | """ 25 | end 26 | 27 | defp to_items(options) do 28 | options 29 | |> Enum.map(&to_item/1) 30 | |> Jason.encode!() 31 | end 32 | 33 | defp to_item({label, value}) do 34 | %{ 35 | label: label, 36 | value: value 37 | } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteInput.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :autocomplete_input, 7 | version: "0.1.0", 8 | elixir: "~> 1.17", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: "A autocomplete input component for Phoenix LiveView", 12 | package: [ 13 | licenses: ["MIT"], 14 | links: %{"Github" => "https://github.com/launchscout/autocomplete_input"} 15 | ], 16 | docs: [ 17 | main: "readme", 18 | extras: ["README.md"] 19 | ] 20 | ] 21 | end 22 | 23 | # Run "mix help compile.app" to learn about applications. 24 | def application do 25 | [ 26 | extra_applications: [:logger] 27 | ] 28 | end 29 | 30 | # Run "mix help deps" to learn about dependencies. 31 | defp deps do 32 | [ 33 | {:phoenix_live_view, "~> 1.0.0"}, 34 | {:live_elements, "~> 0.2.4"} 35 | # {:dep_from_hexpm, "~> 0.3.0"}, 36 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 4 | "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, 5 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 6 | "live_elements": {:hex, :live_elements, "0.2.4", "7e67ea1a48f386fbd17c3989f85a29e4597c4e83ab30e923f6f02135ec1847bd", [:mix], [{:ex_doc, ">= 0.0.0", [hex: :ex_doc, repo: "hexpm", optional: false]}, {:jason, ">= 0.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.1", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "ecc3c2c1fe369785231ceca60af15d0229032ace03f00f259b4049469d1c2cc3"}, 7 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 9 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 10 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 11 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 12 | "phoenix": {:hex, :phoenix, "1.7.18", "5310c21443514be44ed93c422e15870aef254cf1b3619e4f91538e7529d2b2e4", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1797fcc82108442a66f2c77a643a62980f342bfeb63d6c9a515ab8294870004e"}, 13 | "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, 14 | "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.3", "33914a5d47345c7bde056054bca05c0bbbda5ce94a3ee734cb20a4d5d361e20d", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "157d93a5a7c79b836d9b7b5d55c7dd021435139d6160112f0e630f0686b8ea88"}, 15 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 16 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 17 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 18 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 19 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 20 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, 21 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 22 | "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, 23 | } 24 | -------------------------------------------------------------------------------- /test/autocomplete_input_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AutocompleteInputTest do 2 | use ExUnit.Case 3 | doctest AutocompleteInput 4 | 5 | test "greets the world" do 6 | assert AutocompleteInput.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | 2 | ExUnit.start() 3 | --------------------------------------------------------------------------------