├── test ├── test_helper.exs └── click_to_component_test.exs ├── priv ├── demo.gif └── static │ ├── click_to_component.min.js │ ├── click_to_component.esm.js │ ├── click_to_component.cjs.js │ ├── click_to_component.js │ ├── click_to_component.esm.js.map │ └── click_to_component.cjs.js.map ├── .formatter.exs ├── lib ├── click_to_component │ ├── components.ex │ └── hooks.ex └── click_to_component.ex ├── assets ├── package.json └── js │ └── click_to_component │ ├── index.js │ ├── dom.js │ └── menu.js ├── .gitignore ├── package.json ├── config └── config.exs ├── mix.exs ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /priv/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-saas/click_to_component/HEAD/priv/demo.gif -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /test/click_to_component_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClickToComponentTest do 2 | use ExUnit.Case 3 | doctest ClickToComponent 4 | 5 | test "greets the world" do 6 | assert ClickToComponent.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/click_to_component/components.ex: -------------------------------------------------------------------------------- 1 | defmodule ClickToComponent.Components do 2 | use Phoenix.Component 3 | 4 | attr(:id, :string, default: "click-to-component") 5 | 6 | def click_to_component(assigns) do 7 | ~H""" 8 |
9 | """ 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "click_to_component", 3 | "version": "0.1.0", 4 | "description": "Click-to-component functionality for LiveView apps.", 5 | "license": "MIT", 6 | "repository": {}, 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /.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 | click_to_component-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /lib/click_to_component/hooks.ex: -------------------------------------------------------------------------------- 1 | defmodule ClickToComponent.Hooks do 2 | import Phoenix.LiveView 3 | 4 | def on_mount(:default, _params, _session, socket) do 5 | {:cont, attach_hook(socket, :click_to_component, :handle_event, &handle_event/3)} 6 | end 7 | 8 | defp handle_event("click_to_component:open", %{"path" => path}, socket) do 9 | {cmd, args} = command_from_config(path) 10 | System.cmd(cmd, args, cd: File.cwd!()) 11 | 12 | {:halt, socket} 13 | end 14 | 15 | defp handle_event(_event, _params, socket), do: {:cont, socket} 16 | 17 | @default_command {"code", [".", "--goto", :path]} 18 | 19 | def command_from_config(path) do 20 | {cmd, args} = Application.get_env(:click_to_component, :command, @default_command) 21 | {cmd, Enum.map(args, &if(&1 == :path, do: path, else: &1))} 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /assets/js/click_to_component/index.js: -------------------------------------------------------------------------------- 1 | import { createMenu } from "./menu"; 2 | import { findNearestComments } from "./dom"; 3 | 4 | export const ClickToComponent = () => { 5 | return { 6 | mounted() { 7 | this.menu = createMenu(this.onMenuClick.bind(this)); 8 | this.menu.mount(); 9 | 10 | document.addEventListener("click", this.onClick.bind(this)); 11 | }, 12 | 13 | onClick(event) { 14 | if (event.altKey) { 15 | event.preventDefault(); 16 | event.stopPropagation(); 17 | 18 | const comments = findNearestComments(event.target); 19 | 20 | this.menu.render(event, comments); 21 | } 22 | }, 23 | 24 | onMenuClick(comment) { 25 | this.pushEventTo(this.el, "click_to_component:open", { 26 | path: comment.path, 27 | }); 28 | }, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "click_to_component", 3 | "version": "0.1.0", 4 | "description": "Click-to-component functionality for LiveView apps.", 5 | "license": "MIT", 6 | "module": "./priv/static/click_to_component.esm.js", 7 | "main": "./priv/static/click_to_component.cjs.js", 8 | "unpkg": "./priv/static/click_to_component.min.js", 9 | "jsdelivr": "./priv/static/click_to_component.min.js", 10 | "exports": { 11 | "import": "./priv/static/click_to_component.esm.js", 12 | "require": "./priv/static/click_to_component.cjs.js" 13 | }, 14 | "author": "Justin Tormey (https://93software.com)", 15 | "repository": { 16 | "type": "git", 17 | "url": "git://github.com/elixir-saas/click_to_component.git" 18 | }, 19 | "files": [ 20 | "README.md", 21 | "package.json", 22 | "priv/static/*", 23 | "assets/js/click_to_component/*" 24 | ] 25 | } -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :phoenix, :json_library, Jason 4 | config :logger, :level, :debug 5 | config :logger, :backends, [] 6 | 7 | if Mix.env() == :dev do 8 | esbuild = fn args -> 9 | [ 10 | args: ~w(./js/click_to_component --bundle) ++ args, 11 | cd: Path.expand("../assets", __DIR__), 12 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 13 | ] 14 | end 15 | 16 | config :esbuild, 17 | version: "0.12.15", 18 | module: 19 | esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/click_to_component.esm.js)), 20 | main: 21 | esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/click_to_component.cjs.js)), 22 | cdn: 23 | esbuild.( 24 | ~w(--format=iife --target=es2016 --global-name=LiveView --outfile=../priv/static/click_to_component.js) 25 | ), 26 | cdn_min: 27 | esbuild.( 28 | ~w(--format=iife --target=es2016 --global-name=LiveView --minify --outfile=../priv/static/click_to_component.min.js) 29 | ) 30 | end 31 | -------------------------------------------------------------------------------- /assets/js/click_to_component/dom.js: -------------------------------------------------------------------------------- 1 | const parseCommentString = (comment) => { 2 | const [component, path] = comment.trim().split(" "); 3 | 4 | const closing = component.indexOf("/") === 1; 5 | 6 | return { 7 | component: closing ? component.slice(2, -1) : component.slice(1, -1), 8 | path, 9 | closing, 10 | }; 11 | }; 12 | 13 | export const findNearestComments = (currentEl) => { 14 | const accumulated = []; 15 | const closed = []; 16 | 17 | while (true) { 18 | let node = currentEl.previousSibling; 19 | 20 | while (true) { 21 | if (node === null) { 22 | break; 23 | } 24 | 25 | if (node.nodeType === document.COMMENT_NODE) { 26 | const comment = parseCommentString(node.nodeValue); 27 | 28 | if (comment.closing) { 29 | closed.unshift(comment); 30 | } else if (closed[0]?.component === comment.component) { 31 | closed.shift(); 32 | } else { 33 | accumulated.push(comment); 34 | } 35 | } 36 | 37 | node = node.previousSibling; 38 | } 39 | 40 | if (currentEl.parentElement == null) { 41 | break; 42 | } 43 | 44 | currentEl = currentEl.parentElement; 45 | } 46 | 47 | return accumulated; 48 | }; 49 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ClickToComponent.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :click_to_component, 7 | version: "0.2.2", 8 | elixir: "~> 1.15", 9 | start_permanent: Mix.env() == :prod, 10 | name: "Click To Component", 11 | description: "Click-to-component functionality for LiveView apps.", 12 | package: package(), 13 | deps: deps(), 14 | aliases: aliases() 15 | ] 16 | end 17 | 18 | def application do 19 | [ 20 | extra_applications: [:logger] 21 | ] 22 | end 23 | 24 | defp deps do 25 | [ 26 | {:phoenix_live_view, ">= 0.20.0"}, 27 | {:esbuild, "~> 0.2", only: :dev}, 28 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 29 | ] 30 | end 31 | 32 | defp package do 33 | [ 34 | maintainers: ["Justin Tormey"], 35 | licenses: ["MIT"], 36 | links: %{ 37 | GitHub: "https://github.com/elixir-saas/click_to_component" 38 | }, 39 | files: ~w(assets/js lib priv mix.exs package.json README.md) 40 | ] 41 | end 42 | 43 | defp aliases do 44 | [ 45 | "assets.build": ["esbuild module", "esbuild cdn", "esbuild cdn_min", "esbuild main"], 46 | "assets.watch": ["esbuild module --watch"] 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /assets/js/click_to_component/menu.js: -------------------------------------------------------------------------------- 1 | export const createMenu = (onClick) => { 2 | const el = document.createElement("div"); 3 | 4 | Object.assign(el.style, { 5 | display: "none", 6 | position: "fixed", 7 | top: "0px", 8 | left: "0px", 9 | zIndex: 999999999, 10 | background: "white", 11 | border: "1px solid #e4e4e7", 12 | borderRadius: "8px", 13 | padding: "8px 0px", 14 | }); 15 | 16 | return { 17 | mount() { 18 | document.body.appendChild(el); 19 | document.addEventListener("click", (event) => { 20 | if (!el.contains(event.target)) { 21 | el.style.display = "none"; 22 | } 23 | }); 24 | }, 25 | 26 | render(event, comments) { 27 | el.innerHTML = ""; 28 | el.style.top = `${event.clientY}px`; 29 | el.style.left = `${event.clientX}px`; 30 | el.style.display = "block"; 31 | 32 | for (const comment of comments) { 33 | const commentEl = document.createElement("div"); 34 | 35 | Object.assign(commentEl.style, { 36 | margin: "6px 0px", 37 | padding: "0px 12px", 38 | fontSize: "14px", 39 | cursor: "pointer", 40 | }); 41 | 42 | commentEl.innerText = comment.component; 43 | commentEl.onclick = () => onClick(comment); 44 | 45 | el.appendChild(commentEl); 46 | } 47 | }, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /priv/static/click_to_component.min.js: -------------------------------------------------------------------------------- 1 | var LiveView=(()=>{var c=Object.defineProperty;var r=e=>c(e,"__esModule",{value:!0});var m=(e,n)=>{r(e);for(var t in n)c(e,t,{get:n[t],enumerable:!0})};var u={};m(u,{ClickToComponent:()=>a});var p=e=>{let n=document.createElement("div");return Object.assign(n.style,{display:"none",position:"fixed",top:"0px",left:"0px",zIndex:999999999,background:"white",border:"1px solid #e4e4e7",borderRadius:"8px",padding:"8px 0px"}),{mount(){document.body.appendChild(n),document.addEventListener("click",t=>{n.contains(t.target)||(n.style.display="none")})},render(t,s){n.innerHTML="",n.style.top=`${t.clientY}px`,n.style.left=`${t.clientX}px`,n.style.display="block";for(let i of s){let o=document.createElement("div");Object.assign(o.style,{margin:"6px 0px",padding:"0px 12px",fontSize:"14px",cursor:"pointer"}),o.innerText=i.component,o.onclick=()=>e(i),n.appendChild(o)}}}};var d=e=>{let[n,t]=e.trim().split(" "),s=n.indexOf("/")===1;return{component:s?n.slice(2,-1):n.slice(1,-1),path:t,closing:s}},l=e=>{var s;let n=[],t=[];for(;;){let i=e.previousSibling;for(;i!==null;){if(i.nodeType===document.COMMENT_NODE){let o=d(i.nodeValue);o.closing?t.unshift(o):((s=t[0])==null?void 0:s.component)===o.component?t.shift():n.push(o)}i=i.previousSibling}if(e.parentElement==null)break;e=e.parentElement}return n};var a=()=>({mounted(){this.menu=p(this.onMenuClick.bind(this)),this.menu.mount(),document.addEventListener("click",this.onClick.bind(this))},onClick(e){if(e.altKey){e.preventDefault(),e.stopPropagation();let n=l(e.target);this.menu.render(e,n)}},onMenuClick(e){this.pushEventTo(this.el,"click_to_component:open",{path:e.path})}});return u;})(); 2 | -------------------------------------------------------------------------------- /lib/click_to_component.ex: -------------------------------------------------------------------------------- 1 | defmodule ClickToComponent do 2 | @moduledoc """ 3 | Documentation for `ClickToComponent`. 4 | """ 5 | 6 | require Logger 7 | 8 | @enabled? Application.compile_env(:click_to_component, :enabled, false) 9 | 10 | if @enabled? and !Application.compile_env(:phoenix_live_view, :debug_heex_annotations) do 11 | Logger.warning(""" 12 | ClickToComponent requires :debug_heex_annotations to be enabled. Add the following configuration in your `config/dev.exs` file: 13 | 14 | config :phoenix_live_view, debug_heex_annotations: true 15 | """) 16 | end 17 | 18 | if !@enabled? and Application.compile_env(:phoenix_live_view, :debug_heex_annotations) do 19 | Logger.warning(""" 20 | It looks like :debug_heex_annotations is enabled, but ClickToComponent is not. To enable, add the following configuration in your `config/dev.exs` file: 21 | 22 | config :click_to_component, enabled: true 23 | """) 24 | end 25 | 26 | @doc """ 27 | Renders the ClickToComponent hook. 28 | 29 | Add to your `lib/my_app_web/layouts/root.html.heex` file as follows: 30 | 31 | ```html 32 | 33 | 34 | 35 | 36 | ``` 37 | """ 38 | import Phoenix.Component, only: [sigil_H: 2], warn: false 39 | 40 | if @enabled? do 41 | def render(assigns), do: __MODULE__.Components.click_to_component(assigns) 42 | else 43 | def render(assigns), do: ~H"" 44 | end 45 | 46 | @doc """ 47 | Returns the global `on_mount/1` LiveView hook for handling events as quoted code. 48 | 49 | Add to your `lib/my_app_web.ex` file as follows: 50 | 51 | ```elixir 52 | def live_view(opts \\ []) do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {MyAppWeb.Layouts, :app} 56 | 57 | unquote(ClickToComponent.hooks()) 58 | 59 | # Rest of live_view quoted code... 60 | end 61 | end 62 | ``` 63 | """ 64 | if @enabled? do 65 | def hooks() do 66 | quote do 67 | on_mount(unquote(__MODULE__.Hooks)) 68 | end 69 | end 70 | else 71 | def hooks(), do: nil 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveView Click-to-Component 2 | 3 | Adds a click-to-component interaction, which when triggered opens the file and line number associated with the component in your favorite text editor. 4 | 5 | The key binding to trigger the interaction is `Option+Click`. 6 | 7 | ![Demo](./priv/demo.gif) 8 | 9 | ## Installation 10 | 11 | The package can be installed by adding `click_to_component` to your list of dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [ 16 | {:click_to_component, "~> 0.2.2"} 17 | ] 18 | end 19 | ``` 20 | 21 | Documentation can be found at . 22 | 23 | ## Configuration 24 | 25 | To enable in dev, add the following to `config/dev.exs`: 26 | 27 | ```elixir 28 | config :click_to_component, enabled: true 29 | ``` 30 | 31 | You may also customize the command used to open your editor: 32 | 33 | ```elixir 34 | # This is the default command. 35 | # Use `:path` to substitiute one of the args for the path, in the format `file:line` 36 | config :click_to_component, command: {"code", [".", "--goto", :path]} 37 | ``` 38 | 39 | Make sure that you have LiveView >=0.20.0 installed in your project, and that you have enabled debug annotations: 40 | 41 | ```elixir 42 | config :phoenix_live_view, debug_heex_annotations: true 43 | ``` 44 | 45 | ## Usage 46 | 47 | First, add the following code to your `lib/my_app_web.ex` module, in the quoted code in your `live_view` function: 48 | 49 | ```elixir 50 | def live_view(opts \\ []) do 51 | quote do 52 | use Phoenix.LiveView, 53 | layout: {MyAppWeb.Layouts, :app} 54 | 55 | # Add this line 56 | unquote(ClickToComponent.hooks()) 57 | 58 | # Rest of live_view quoted code... 59 | end 60 | end 61 | ``` 62 | 63 | Next, render the component code in your `lib/my_app_web/layouts/root.html.heex` layout file: 64 | 65 | ```html 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | When `click_to_component` is enabled, this will render a JS hook that listens for clicks and renders the component menu on click. 73 | 74 | Finally, import and configure the `ClickToComponent` hook in `assets/js/app.js`: 75 | 76 | ```js 77 | import { ClickToComponent } from "click_to_component"; 78 | 79 | // Note the function call! 80 | const hooks = { 81 | ClickToComponent: ClickToComponent(), 82 | }; 83 | 84 | // Make sure `hooks` is configured in LiveSocket 85 | let liveSocket = new LiveSocket(socketUrl, Socket, { 86 | hooks, 87 | params: { _csrf_token: csrfToken }, 88 | }); 89 | ``` 90 | -------------------------------------------------------------------------------- /priv/static/click_to_component.esm.js: -------------------------------------------------------------------------------- 1 | // js/click_to_component/menu.js 2 | var createMenu = (onClick) => { 3 | const el = document.createElement("div"); 4 | Object.assign(el.style, { 5 | display: "none", 6 | position: "fixed", 7 | top: "0px", 8 | left: "0px", 9 | zIndex: 999999999, 10 | background: "white", 11 | border: "1px solid #e4e4e7", 12 | borderRadius: "8px", 13 | padding: "8px 0px" 14 | }); 15 | return { 16 | mount() { 17 | document.body.appendChild(el); 18 | document.addEventListener("click", (event) => { 19 | if (!el.contains(event.target)) { 20 | el.style.display = "none"; 21 | } 22 | }); 23 | }, 24 | render(event, comments) { 25 | el.innerHTML = ""; 26 | el.style.top = `${event.clientY}px`; 27 | el.style.left = `${event.clientX}px`; 28 | el.style.display = "block"; 29 | for (const comment of comments) { 30 | const commentEl = document.createElement("div"); 31 | Object.assign(commentEl.style, { 32 | margin: "6px 0px", 33 | padding: "0px 12px", 34 | fontSize: "14px", 35 | cursor: "pointer" 36 | }); 37 | commentEl.innerText = comment.component; 38 | commentEl.onclick = () => onClick(comment); 39 | el.appendChild(commentEl); 40 | } 41 | } 42 | }; 43 | }; 44 | 45 | // js/click_to_component/dom.js 46 | var parseCommentString = (comment) => { 47 | const [component, path] = comment.trim().split(" "); 48 | const closing = component.indexOf("/") === 1; 49 | return { 50 | component: closing ? component.slice(2, -1) : component.slice(1, -1), 51 | path, 52 | closing 53 | }; 54 | }; 55 | var findNearestComments = (currentEl) => { 56 | const accumulated = []; 57 | const closed = []; 58 | while (true) { 59 | let node = currentEl.previousSibling; 60 | while (true) { 61 | if (node === null) { 62 | break; 63 | } 64 | if (node.nodeType === document.COMMENT_NODE) { 65 | const comment = parseCommentString(node.nodeValue); 66 | if (comment.closing) { 67 | closed.unshift(comment); 68 | } else if (closed[0]?.component === comment.component) { 69 | closed.shift(); 70 | } else { 71 | accumulated.push(comment); 72 | } 73 | } 74 | node = node.previousSibling; 75 | } 76 | if (currentEl.parentElement == null) { 77 | break; 78 | } 79 | currentEl = currentEl.parentElement; 80 | } 81 | return accumulated; 82 | }; 83 | 84 | // js/click_to_component/index.js 85 | var ClickToComponent = () => { 86 | return { 87 | mounted() { 88 | this.menu = createMenu(this.onMenuClick.bind(this)); 89 | this.menu.mount(); 90 | document.addEventListener("click", this.onClick.bind(this)); 91 | }, 92 | onClick(event) { 93 | if (event.altKey) { 94 | event.preventDefault(); 95 | event.stopPropagation(); 96 | const comments = findNearestComments(event.target); 97 | this.menu.render(event, comments); 98 | } 99 | }, 100 | onMenuClick(comment) { 101 | this.pushEventTo(this.el, "click_to_component:open", { 102 | path: comment.path 103 | }); 104 | } 105 | }; 106 | }; 107 | export { 108 | ClickToComponent 109 | }; 110 | //# sourceMappingURL=click_to_component.esm.js.map 111 | -------------------------------------------------------------------------------- /priv/static/click_to_component.cjs.js: -------------------------------------------------------------------------------- 1 | var __defProp = Object.defineProperty; 2 | var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); 3 | var __export = (target, all) => { 4 | __markAsModule(target); 5 | for (var name in all) 6 | __defProp(target, name, { get: all[name], enumerable: true }); 7 | }; 8 | 9 | // js/click_to_component/index.js 10 | __export(exports, { 11 | ClickToComponent: () => ClickToComponent 12 | }); 13 | 14 | // js/click_to_component/menu.js 15 | var createMenu = (onClick) => { 16 | const el = document.createElement("div"); 17 | Object.assign(el.style, { 18 | display: "none", 19 | position: "fixed", 20 | top: "0px", 21 | left: "0px", 22 | zIndex: 999999999, 23 | background: "white", 24 | border: "1px solid #e4e4e7", 25 | borderRadius: "8px", 26 | padding: "8px 0px" 27 | }); 28 | return { 29 | mount() { 30 | document.body.appendChild(el); 31 | document.addEventListener("click", (event) => { 32 | if (!el.contains(event.target)) { 33 | el.style.display = "none"; 34 | } 35 | }); 36 | }, 37 | render(event, comments) { 38 | el.innerHTML = ""; 39 | el.style.top = `${event.clientY}px`; 40 | el.style.left = `${event.clientX}px`; 41 | el.style.display = "block"; 42 | for (const comment of comments) { 43 | const commentEl = document.createElement("div"); 44 | Object.assign(commentEl.style, { 45 | margin: "6px 0px", 46 | padding: "0px 12px", 47 | fontSize: "14px", 48 | cursor: "pointer" 49 | }); 50 | commentEl.innerText = comment.component; 51 | commentEl.onclick = () => onClick(comment); 52 | el.appendChild(commentEl); 53 | } 54 | } 55 | }; 56 | }; 57 | 58 | // js/click_to_component/dom.js 59 | var parseCommentString = (comment) => { 60 | const [component, path] = comment.trim().split(" "); 61 | const closing = component.indexOf("/") === 1; 62 | return { 63 | component: closing ? component.slice(2, -1) : component.slice(1, -1), 64 | path, 65 | closing 66 | }; 67 | }; 68 | var findNearestComments = (currentEl) => { 69 | const accumulated = []; 70 | const closed = []; 71 | while (true) { 72 | let node = currentEl.previousSibling; 73 | while (true) { 74 | if (node === null) { 75 | break; 76 | } 77 | if (node.nodeType === document.COMMENT_NODE) { 78 | const comment = parseCommentString(node.nodeValue); 79 | if (comment.closing) { 80 | closed.unshift(comment); 81 | } else if (closed[0]?.component === comment.component) { 82 | closed.shift(); 83 | } else { 84 | accumulated.push(comment); 85 | } 86 | } 87 | node = node.previousSibling; 88 | } 89 | if (currentEl.parentElement == null) { 90 | break; 91 | } 92 | currentEl = currentEl.parentElement; 93 | } 94 | return accumulated; 95 | }; 96 | 97 | // js/click_to_component/index.js 98 | var ClickToComponent = () => { 99 | return { 100 | mounted() { 101 | this.menu = createMenu(this.onMenuClick.bind(this)); 102 | this.menu.mount(); 103 | document.addEventListener("click", this.onClick.bind(this)); 104 | }, 105 | onClick(event) { 106 | if (event.altKey) { 107 | event.preventDefault(); 108 | event.stopPropagation(); 109 | const comments = findNearestComments(event.target); 110 | this.menu.render(event, comments); 111 | } 112 | }, 113 | onMenuClick(comment) { 114 | this.pushEventTo(this.el, "click_to_component:open", { 115 | path: comment.path 116 | }); 117 | } 118 | }; 119 | }; 120 | //# sourceMappingURL=click_to_component.cjs.js.map 121 | -------------------------------------------------------------------------------- /priv/static/click_to_component.js: -------------------------------------------------------------------------------- 1 | var LiveView = (() => { 2 | var __defProp = Object.defineProperty; 3 | var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); 4 | var __export = (target, all) => { 5 | __markAsModule(target); 6 | for (var name in all) 7 | __defProp(target, name, { get: all[name], enumerable: true }); 8 | }; 9 | 10 | // js/click_to_component/index.js 11 | var click_to_component_exports = {}; 12 | __export(click_to_component_exports, { 13 | ClickToComponent: () => ClickToComponent 14 | }); 15 | 16 | // js/click_to_component/menu.js 17 | var createMenu = (onClick) => { 18 | const el = document.createElement("div"); 19 | Object.assign(el.style, { 20 | display: "none", 21 | position: "fixed", 22 | top: "0px", 23 | left: "0px", 24 | zIndex: 999999999, 25 | background: "white", 26 | border: "1px solid #e4e4e7", 27 | borderRadius: "8px", 28 | padding: "8px 0px" 29 | }); 30 | return { 31 | mount() { 32 | document.body.appendChild(el); 33 | document.addEventListener("click", (event) => { 34 | if (!el.contains(event.target)) { 35 | el.style.display = "none"; 36 | } 37 | }); 38 | }, 39 | render(event, comments) { 40 | el.innerHTML = ""; 41 | el.style.top = `${event.clientY}px`; 42 | el.style.left = `${event.clientX}px`; 43 | el.style.display = "block"; 44 | for (const comment of comments) { 45 | const commentEl = document.createElement("div"); 46 | Object.assign(commentEl.style, { 47 | margin: "6px 0px", 48 | padding: "0px 12px", 49 | fontSize: "14px", 50 | cursor: "pointer" 51 | }); 52 | commentEl.innerText = comment.component; 53 | commentEl.onclick = () => onClick(comment); 54 | el.appendChild(commentEl); 55 | } 56 | } 57 | }; 58 | }; 59 | 60 | // js/click_to_component/dom.js 61 | var parseCommentString = (comment) => { 62 | const [component, path] = comment.trim().split(" "); 63 | const closing = component.indexOf("/") === 1; 64 | return { 65 | component: closing ? component.slice(2, -1) : component.slice(1, -1), 66 | path, 67 | closing 68 | }; 69 | }; 70 | var findNearestComments = (currentEl) => { 71 | var _a; 72 | const accumulated = []; 73 | const closed = []; 74 | while (true) { 75 | let node = currentEl.previousSibling; 76 | while (true) { 77 | if (node === null) { 78 | break; 79 | } 80 | if (node.nodeType === document.COMMENT_NODE) { 81 | const comment = parseCommentString(node.nodeValue); 82 | if (comment.closing) { 83 | closed.unshift(comment); 84 | } else if (((_a = closed[0]) == null ? void 0 : _a.component) === comment.component) { 85 | closed.shift(); 86 | } else { 87 | accumulated.push(comment); 88 | } 89 | } 90 | node = node.previousSibling; 91 | } 92 | if (currentEl.parentElement == null) { 93 | break; 94 | } 95 | currentEl = currentEl.parentElement; 96 | } 97 | return accumulated; 98 | }; 99 | 100 | // js/click_to_component/index.js 101 | var ClickToComponent = () => { 102 | return { 103 | mounted() { 104 | this.menu = createMenu(this.onMenuClick.bind(this)); 105 | this.menu.mount(); 106 | document.addEventListener("click", this.onClick.bind(this)); 107 | }, 108 | onClick(event) { 109 | if (event.altKey) { 110 | event.preventDefault(); 111 | event.stopPropagation(); 112 | const comments = findNearestComments(event.target); 113 | this.menu.render(event, comments); 114 | } 115 | }, 116 | onMenuClick(comment) { 117 | this.pushEventTo(this.el, "click_to_component:open", { 118 | path: comment.path 119 | }); 120 | } 121 | }; 122 | }; 123 | return click_to_component_exports; 124 | })(); 125 | -------------------------------------------------------------------------------- /priv/static/click_to_component.esm.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../assets/js/click_to_component/menu.js", "../../assets/js/click_to_component/dom.js", "../../assets/js/click_to_component/index.js"], 4 | "sourcesContent": ["export const createMenu = (onClick) => {\n const el = document.createElement(\"div\");\n\n Object.assign(el.style, {\n display: \"none\",\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n zIndex: 999999999,\n background: \"white\",\n border: \"1px solid #e4e4e7\",\n borderRadius: \"8px\",\n padding: \"8px 0px\",\n });\n\n return {\n mount() {\n document.body.appendChild(el);\n document.addEventListener(\"click\", (event) => {\n if (!el.contains(event.target)) {\n el.style.display = \"none\";\n }\n });\n },\n\n render(event, comments) {\n el.innerHTML = \"\";\n el.style.top = `${event.clientY}px`;\n el.style.left = `${event.clientX}px`;\n el.style.display = \"block\";\n\n for (const comment of comments) {\n const commentEl = document.createElement(\"div\");\n\n Object.assign(commentEl.style, {\n margin: \"6px 0px\",\n padding: \"0px 12px\",\n fontSize: \"14px\",\n cursor: \"pointer\",\n });\n\n commentEl.innerText = comment.component;\n commentEl.onclick = () => onClick(comment);\n\n el.appendChild(commentEl);\n }\n },\n };\n};\n", "const parseCommentString = (comment) => {\n const [component, path] = comment.trim().split(\" \");\n\n const closing = component.indexOf(\"/\") === 1;\n\n return {\n component: closing ? component.slice(2, -1) : component.slice(1, -1),\n path,\n closing,\n };\n};\n\nexport const findNearestComments = (currentEl) => {\n const accumulated = [];\n const closed = [];\n\n while (true) {\n let node = currentEl.previousSibling;\n\n while (true) {\n if (node === null) {\n break;\n }\n\n if (node.nodeType === document.COMMENT_NODE) {\n const comment = parseCommentString(node.nodeValue);\n\n if (comment.closing) {\n closed.unshift(comment);\n } else if (closed[0]?.component === comment.component) {\n closed.shift();\n } else {\n accumulated.push(comment);\n }\n }\n\n node = node.previousSibling;\n }\n\n if (currentEl.parentElement == null) {\n break;\n }\n\n currentEl = currentEl.parentElement;\n }\n\n return accumulated;\n};\n", "import { createMenu } from \"./menu\";\nimport { findNearestComments } from \"./dom\";\n\nexport const ClickToComponent = () => {\n return {\n mounted() {\n this.menu = createMenu(this.onMenuClick.bind(this));\n this.menu.mount();\n\n document.addEventListener(\"click\", this.onClick.bind(this));\n },\n\n onClick(event) {\n if (event.altKey) {\n event.preventDefault();\n event.stopPropagation();\n\n const comments = findNearestComments(event.target);\n\n this.menu.render(event, comments);\n }\n },\n\n onMenuClick(comment) {\n this.pushEventTo(this.el, \"click_to_component:open\", {\n path: comment.path,\n });\n },\n };\n};\n"], 5 | "mappings": ";AAAO,IAAM,aAAa,CAAC,YAAY;AACrC,QAAM,KAAK,SAAS,cAAc;AAElC,SAAO,OAAO,GAAG,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,SAAS;AAAA;AAGX,SAAO;AAAA,IACL,QAAQ;AACN,eAAS,KAAK,YAAY;AAC1B,eAAS,iBAAiB,SAAS,CAAC,UAAU;AAC5C,YAAI,CAAC,GAAG,SAAS,MAAM,SAAS;AAC9B,aAAG,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,IAKzB,OAAO,OAAO,UAAU;AACtB,SAAG,YAAY;AACf,SAAG,MAAM,MAAM,GAAG,MAAM;AACxB,SAAG,MAAM,OAAO,GAAG,MAAM;AACzB,SAAG,MAAM,UAAU;AAEnB,iBAAW,WAAW,UAAU;AAC9B,cAAM,YAAY,SAAS,cAAc;AAEzC,eAAO,OAAO,UAAU,OAAO;AAAA,UAC7B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,QAAQ;AAAA;AAGV,kBAAU,YAAY,QAAQ;AAC9B,kBAAU,UAAU,MAAM,QAAQ;AAElC,WAAG,YAAY;AAAA;AAAA;AAAA;AAAA;;;AC5CvB,IAAM,qBAAqB,CAAC,YAAY;AACtC,QAAM,CAAC,WAAW,QAAQ,QAAQ,OAAO,MAAM;AAE/C,QAAM,UAAU,UAAU,QAAQ,SAAS;AAE3C,SAAO;AAAA,IACL,WAAW,UAAU,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,GAAG;AAAA,IACjE;AAAA,IACA;AAAA;AAAA;AAIG,IAAM,sBAAsB,CAAC,cAAc;AAChD,QAAM,cAAc;AACpB,QAAM,SAAS;AAEf,SAAO,MAAM;AACX,QAAI,OAAO,UAAU;AAErB,WAAO,MAAM;AACX,UAAI,SAAS,MAAM;AACjB;AAAA;AAGF,UAAI,KAAK,aAAa,SAAS,cAAc;AAC3C,cAAM,UAAU,mBAAmB,KAAK;AAExC,YAAI,QAAQ,SAAS;AACnB,iBAAO,QAAQ;AAAA,mBACN,OAAO,IAAI,cAAc,QAAQ,WAAW;AACrD,iBAAO;AAAA,eACF;AACL,sBAAY,KAAK;AAAA;AAAA;AAIrB,aAAO,KAAK;AAAA;AAGd,QAAI,UAAU,iBAAiB,MAAM;AACnC;AAAA;AAGF,gBAAY,UAAU;AAAA;AAGxB,SAAO;AAAA;;;AC3CF,IAAM,mBAAmB,MAAM;AACpC,SAAO;AAAA,IACL,UAAU;AACR,WAAK,OAAO,WAAW,KAAK,YAAY,KAAK;AAC7C,WAAK,KAAK;AAEV,eAAS,iBAAiB,SAAS,KAAK,QAAQ,KAAK;AAAA;AAAA,IAGvD,QAAQ,OAAO;AACb,UAAI,MAAM,QAAQ;AAChB,cAAM;AACN,cAAM;AAEN,cAAM,WAAW,oBAAoB,MAAM;AAE3C,aAAK,KAAK,OAAO,OAAO;AAAA;AAAA;AAAA,IAI5B,YAAY,SAAS;AACnB,WAAK,YAAY,KAAK,IAAI,2BAA2B;AAAA,QACnD,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /priv/static/click_to_component.cjs.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../assets/js/click_to_component/index.js", "../../assets/js/click_to_component/menu.js", "../../assets/js/click_to_component/dom.js"], 4 | "sourcesContent": ["import { createMenu } from \"./menu\";\nimport { findNearestComments } from \"./dom\";\n\nexport const ClickToComponent = () => {\n return {\n mounted() {\n this.menu = createMenu(this.onMenuClick.bind(this));\n this.menu.mount();\n\n document.addEventListener(\"click\", this.onClick.bind(this));\n },\n\n onClick(event) {\n if (event.altKey) {\n event.preventDefault();\n event.stopPropagation();\n\n const comments = findNearestComments(event.target);\n\n this.menu.render(event, comments);\n }\n },\n\n onMenuClick(comment) {\n this.pushEventTo(this.el, \"click_to_component:open\", {\n path: comment.path,\n });\n },\n };\n};\n", "export const createMenu = (onClick) => {\n const el = document.createElement(\"div\");\n\n Object.assign(el.style, {\n display: \"none\",\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n zIndex: 999999999,\n background: \"white\",\n border: \"1px solid #e4e4e7\",\n borderRadius: \"8px\",\n padding: \"8px 0px\",\n });\n\n return {\n mount() {\n document.body.appendChild(el);\n document.addEventListener(\"click\", (event) => {\n if (!el.contains(event.target)) {\n el.style.display = \"none\";\n }\n });\n },\n\n render(event, comments) {\n el.innerHTML = \"\";\n el.style.top = `${event.clientY}px`;\n el.style.left = `${event.clientX}px`;\n el.style.display = \"block\";\n\n for (const comment of comments) {\n const commentEl = document.createElement(\"div\");\n\n Object.assign(commentEl.style, {\n margin: \"6px 0px\",\n padding: \"0px 12px\",\n fontSize: \"14px\",\n cursor: \"pointer\",\n });\n\n commentEl.innerText = comment.component;\n commentEl.onclick = () => onClick(comment);\n\n el.appendChild(commentEl);\n }\n },\n };\n};\n", "const parseCommentString = (comment) => {\n const [component, path] = comment.trim().split(\" \");\n\n const closing = component.indexOf(\"/\") === 1;\n\n return {\n component: closing ? component.slice(2, -1) : component.slice(1, -1),\n path,\n closing,\n };\n};\n\nexport const findNearestComments = (currentEl) => {\n const accumulated = [];\n const closed = [];\n\n while (true) {\n let node = currentEl.previousSibling;\n\n while (true) {\n if (node === null) {\n break;\n }\n\n if (node.nodeType === document.COMMENT_NODE) {\n const comment = parseCommentString(node.nodeValue);\n\n if (comment.closing) {\n closed.unshift(comment);\n } else if (closed[0]?.component === comment.component) {\n closed.shift();\n } else {\n accumulated.push(comment);\n }\n }\n\n node = node.previousSibling;\n }\n\n if (currentEl.parentElement == null) {\n break;\n }\n\n currentEl = currentEl.parentElement;\n }\n\n return accumulated;\n};\n"], 5 | "mappings": ";;;;;;;;;AAAA;AAAA;AAAA;;;ACAO,IAAM,aAAa,CAAC,YAAY;AACrC,QAAM,KAAK,SAAS,cAAc;AAElC,SAAO,OAAO,GAAG,OAAO;AAAA,IACtB,SAAS;AAAA,IACT,UAAU;AAAA,IACV,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,SAAS;AAAA;AAGX,SAAO;AAAA,IACL,QAAQ;AACN,eAAS,KAAK,YAAY;AAC1B,eAAS,iBAAiB,SAAS,CAAC,UAAU;AAC5C,YAAI,CAAC,GAAG,SAAS,MAAM,SAAS;AAC9B,aAAG,MAAM,UAAU;AAAA;AAAA;AAAA;AAAA,IAKzB,OAAO,OAAO,UAAU;AACtB,SAAG,YAAY;AACf,SAAG,MAAM,MAAM,GAAG,MAAM;AACxB,SAAG,MAAM,OAAO,GAAG,MAAM;AACzB,SAAG,MAAM,UAAU;AAEnB,iBAAW,WAAW,UAAU;AAC9B,cAAM,YAAY,SAAS,cAAc;AAEzC,eAAO,OAAO,UAAU,OAAO;AAAA,UAC7B,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,UAAU;AAAA,UACV,QAAQ;AAAA;AAGV,kBAAU,YAAY,QAAQ;AAC9B,kBAAU,UAAU,MAAM,QAAQ;AAElC,WAAG,YAAY;AAAA;AAAA;AAAA;AAAA;;;AC5CvB,IAAM,qBAAqB,CAAC,YAAY;AACtC,QAAM,CAAC,WAAW,QAAQ,QAAQ,OAAO,MAAM;AAE/C,QAAM,UAAU,UAAU,QAAQ,SAAS;AAE3C,SAAO;AAAA,IACL,WAAW,UAAU,UAAU,MAAM,GAAG,MAAM,UAAU,MAAM,GAAG;AAAA,IACjE;AAAA,IACA;AAAA;AAAA;AAIG,IAAM,sBAAsB,CAAC,cAAc;AAChD,QAAM,cAAc;AACpB,QAAM,SAAS;AAEf,SAAO,MAAM;AACX,QAAI,OAAO,UAAU;AAErB,WAAO,MAAM;AACX,UAAI,SAAS,MAAM;AACjB;AAAA;AAGF,UAAI,KAAK,aAAa,SAAS,cAAc;AAC3C,cAAM,UAAU,mBAAmB,KAAK;AAExC,YAAI,QAAQ,SAAS;AACnB,iBAAO,QAAQ;AAAA,mBACN,OAAO,IAAI,cAAc,QAAQ,WAAW;AACrD,iBAAO;AAAA,eACF;AACL,sBAAY,KAAK;AAAA;AAAA;AAIrB,aAAO,KAAK;AAAA;AAGd,QAAI,UAAU,iBAAiB,MAAM;AACnC;AAAA;AAGF,gBAAY,UAAU;AAAA;AAGxB,SAAO;AAAA;;;AF3CF,IAAM,mBAAmB,MAAM;AACpC,SAAO;AAAA,IACL,UAAU;AACR,WAAK,OAAO,WAAW,KAAK,YAAY,KAAK;AAC7C,WAAK,KAAK;AAEV,eAAS,iBAAiB,SAAS,KAAK,QAAQ,KAAK;AAAA;AAAA,IAGvD,QAAQ,OAAO;AACb,UAAI,MAAM,QAAQ;AAChB,cAAM;AACN,cAAM;AAEN,cAAM,WAAW,oBAAoB,MAAM;AAE3C,aAAK,KAAK,OAAO,OAAO;AAAA;AAAA;AAAA,IAI5B,YAAY,SAAS;AACnB,WAAK,YAAY,KAAK,IAAI,2BAA2B;AAAA,QACnD,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"}, 4 | "esbuild": {:hex, :esbuild, "0.7.1", "fa0947e8c3c3c2f86c9bf7e791a0a385007ccd42b86885e8e893bdb6631f5169", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "66661cdf70b1378ee4dc16573fcee67750b59761b2605a0207c267ab9d19f13c"}, 5 | "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, 6 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [: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", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 9 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 11 | "phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [: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.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [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", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"}, 12 | "phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"}, 13 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.0", "3f3531c835e46a3b45b4c3ca4a09cef7ba1d0f0d0035eef751c7084b8adb1299", [:mix], [{: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", [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]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "29875f8a58fb031f2dc8f3be025c92ed78d342b46f9bbf6dfe579549d7c81050"}, 14 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 15 | "phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"}, 16 | "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [: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", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, 17 | "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, 18 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 19 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 20 | "websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [: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", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"}, 21 | } 22 | --------------------------------------------------------------------------------