-
-
Save SteffenDE/f599405c7c2eddbb14723ed4f3b7213f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Application.put_env(:sample, Example.Endpoint, | |
| http: [ip: {127, 0, 0, 1}, port: 5001], | |
| server: true, | |
| live_view: [signing_salt: "aaaaaaaa"], | |
| secret_key_base: String.duplicate("a", 64) | |
| ) | |
| Mix.install( | |
| [ | |
| {:plug_cowboy, "~> 2.5"}, | |
| {:jason, "~> 1.0"}, | |
| {:phoenix, "~> 1.7"}, | |
| {:phoenix_live_view, | |
| github: "phoenixframework/phoenix_live_view", branch: "main", override: true} | |
| ] | |
| ) | |
| defmodule Example.ErrorView do | |
| def render(template, _), do: Phoenix.Controller.status_message_from_template(template) | |
| end | |
| defmodule Example.PortalLive do | |
| use Phoenix.LiveView | |
| alias Phoenix.LiveView.JS | |
| def render("root.html", assigns) do | |
| ~H""" | |
| <!DOCTYPE html> | |
| <head> | |
| <meta name="csrf-token" content={Plug.CSRFProtection.get_csrf_token()} /> | |
| <script src="https://cdn.tailwindcss.com/3.4.3"> | |
| </script> | |
| <script src="/assets/phoenix/phoenix.min.js"> | |
| </script> | |
| <script type="module"> | |
| import { LiveSocket } from "/assets/phoenix_live_view/phoenix_live_view.esm.js"; | |
| import { computePosition, autoUpdate, offset } from "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.0/+esm"; | |
| let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); | |
| let liveSocket = new LiveSocket("/live", window.Phoenix.Socket, { | |
| params: {_csrf_token: csrfToken}, | |
| hooks: { | |
| PortalTooltip: { | |
| mounted() { | |
| this.tooltipEl = document.getElementById(this.el.dataset.id); | |
| this.activatorEl = this.el.querySelector(`#${this.el.dataset.id}-activator`); | |
| this.activatorEl.addEventListener("focusin", () => this.queueShow()); | |
| this.activatorEl.addEventListener("mouseover", () => this.queueShow()); | |
| this.activatorEl.addEventListener("focusout", () => this.queueHide()); | |
| this.activatorEl.addEventListener("mouseout", () => this.queueHide()); | |
| this.el.addEventListener("phx:hide-tooltip", () => this.hide()); | |
| }, | |
| destroyed() { | |
| this.cleanup && this.cleanup(); | |
| }, | |
| queueShow() { | |
| clearTimeout(this.hideTimeout); | |
| this.showTimeout = setTimeout(() => this.show(), 200); | |
| }, | |
| queueHide() { | |
| clearTimeout(this.showTimeout); | |
| this.hideTimeout = setTimeout(() => this.hide(), 50); | |
| }, | |
| show() { | |
| this.cleanup && this.cleanup(); | |
| this.cleanup = autoUpdate(this.activatorEl, this.tooltipEl, () => { | |
| computePosition(this.activatorEl, this.tooltipEl, { | |
| placement: this.el.dataset.position, | |
| middleware: [offset(10)] | |
| }).then(({ x, y }) => { | |
| this.tooltipEl.style.left = `${x}px`; | |
| this.tooltipEl.style.top = `${y}px`; | |
| }); | |
| }); | |
| this.liveSocket.execJS(this.el, this.el.dataset.show); | |
| }, | |
| hide() { | |
| this.liveSocket.execJS(this.el, this.el.dataset.hide); | |
| this.cleanup && this.cleanup(); | |
| }, | |
| } | |
| } | |
| }) | |
| liveSocket.connect() | |
| window.liveSocket = liveSocket | |
| </script> | |
| </head> | |
| <body> | |
| <main style="flex: 1; padding: 2rem;"> | |
| {@inner_content} | |
| </main> | |
| <div id="root-portal"></div> | |
| </body> | |
| """ | |
| end | |
| def render("live.html", assigns) do | |
| ~H""" | |
| {@inner_content} | |
| <div id="portal-target"></div> | |
| """ | |
| end | |
| @impl Phoenix.LiveView | |
| def mount(params, _session, socket) do | |
| case params do | |
| %{"tick" => "false"} -> :ok | |
| _ -> :timer.send_interval(1000, self(), :tick) | |
| end | |
| socket | |
| |> assign(:count, 0) | |
| |> assign(:render_modal, true) | |
| |> then(&{:ok, &1, layout: {__MODULE__, :live}}) | |
| end | |
| @impl Phoenix.LiveView | |
| def handle_info(:tick, socket) do | |
| {:noreply, assign(socket, :count, socket.assigns.count + 1)} | |
| end | |
| @impl Phoenix.LiveView | |
| def render(assigns) do | |
| ~H""" | |
| <div class="border border-sky-600 overflow-hidden mt-8 p-4 flex gap-4"> | |
| <.tooltip id="tooltip-example-portal"> | |
| <:activator> | |
| <.button>Hover me</.button> | |
| </:activator> | |
| Hey there! {@count} | |
| </.tooltip> | |
| <.tooltip id="tooltip-example-no-portal" portal={false}> | |
| <:activator> | |
| <.button>Hover me (no portal)</.button> | |
| </:activator> | |
| Hey there! {@count} | |
| </.tooltip> | |
| </div> | |
| """ | |
| end | |
| attr :type, :string, default: nil | |
| attr :class, :string, default: nil | |
| attr :rest, :global, include: ~w(disabled form name value) | |
| slot :inner_block, required: true | |
| def button(assigns) do | |
| ~H""" | |
| <button | |
| type={@type} | |
| class={[ | |
| "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3", | |
| "text-sm font-semibold leading-6 text-white active:text-white/80", | |
| @class | |
| ]} | |
| {@rest} | |
| > | |
| {render_slot(@inner_block)} | |
| </button> | |
| """ | |
| end | |
| attr :id, :string, required: true | |
| attr :portal, :boolean, default: true | |
| slot :activator, required: true | |
| slot :inner_block, required: true | |
| def tooltip(assigns) do | |
| ~H""" | |
| <div | |
| id={"#{@id}-wrapper"} | |
| class="relative inline-block w-fit" | |
| phx-hook="PortalTooltip" | |
| data-id={@id} | |
| data-show={show_tooltip(@id)} | |
| data-hide={hide_tooltip(@id)} | |
| data-position="top" | |
| phx-window-keydown={JS.dispatch("phx:hide-tooltip")} | |
| phx-key="escape" | |
| > | |
| <div id={"#{@id}-activator"} aria-describedby={@id} data-activator> | |
| {render_slot(@activator)} | |
| </div> | |
| <.portal :if={@portal} id={"#{@id}-portal"} target="body"> | |
| <div | |
| id={@id} | |
| phx-mounted={JS.ignore_attributes(["style"])} | |
| role="tooltip" | |
| class="hidden absolute top-0 left-0 z-50 bg-sky-800 text-white text-xs p-1" | |
| > | |
| {render_slot(@inner_block)} | |
| </div> | |
| </.portal> | |
| <div | |
| :if={!@portal} | |
| id={@id} | |
| phx-mounted={JS.ignore_attributes(["style"])} | |
| role="tooltip" | |
| class="hidden absolute top-0 left-0 z-50 bg-sky-800 text-white text-xs p-1" | |
| > | |
| {render_slot(@inner_block)} | |
| </div> | |
| </div> | |
| """ | |
| end | |
| defp show_tooltip(id) do | |
| JS.show( | |
| to: "##{id}", | |
| transition: | |
| {"transform ease-out duration-200 transition origin-bottom", | |
| "scale-95 translate-y-0.5 opacity-0", "scale-100 translate-y-0 opacity-100"}, | |
| display: "block", | |
| time: 200, | |
| blocking: false | |
| ) | |
| end | |
| def hide_tooltip(id) do | |
| JS.hide( | |
| to: "##{id}", | |
| transition: {"transition ease-in duration-100", "opacity-100", "opacity-0"}, | |
| time: 100, | |
| blocking: false | |
| ) | |
| end | |
| end | |
| defmodule Example.Router do | |
| use Phoenix.Router | |
| import Phoenix.LiveView.Router | |
| pipeline :browser do | |
| plug(:accepts, ["html"]) | |
| plug :put_root_layout, {Example.PortalLive, :root} | |
| end | |
| scope "/", Example do | |
| pipe_through(:browser) | |
| live("/", PortalLive, :index) | |
| end | |
| end | |
| defmodule Example.Endpoint do | |
| use Phoenix.Endpoint, otp_app: :sample | |
| socket("/live", Phoenix.LiveView.Socket) | |
| plug Plug.Static, from: {:phoenix, "priv/static"}, at: "/assets/phoenix" | |
| plug Plug.Static, from: {:phoenix_live_view, "priv/static"}, at: "/assets/phoenix_live_view" | |
| plug(Example.Router) | |
| end | |
| {:ok, _} = Supervisor.start_link([Example.Endpoint], strategy: :one_for_one) | |
| Process.sleep(:infinity) |
Author
@absowoot thank you, we changed the target to allow a selector instead of an id very shortly before the release. I adjusted the target to "body" instead, since the separate container is also not necessary any more :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quick note -- line 189 should be: