Packages

phoenix_kit

1.7.177
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit_web components media_browser embed.ex
Raw

lib/phoenix_kit_web/components/media_browser/embed.ex

defmodule PhoenixKitWeb.Components.MediaBrowser.Embed do
@moduledoc """
One-line embedder for `PhoenixKitWeb.Components.MediaBrowser`.
LiveView uploads must live on the parent socket (not the LiveComponent),
so embedding the MediaBrowser requires three small pieces of plumbing on
the parent: an `allow_upload`, a `"validate"` event stub for the upload
channel, and a `handle_info` delegator for component→parent messages.
This module bundles all three into a single `use` call.
## Usage
defmodule MyAppWeb.MediaPage do
use MyAppWeb, :live_view
use PhoenixKitWeb.Components.MediaBrowser.Embed
def mount(_params, _session, socket) do
{:ok, socket}
end
end
Then in the template:
<.live_component
module={PhoenixKitWeb.Components.MediaBrowser}
id="media-browser"
parent_uploads={@uploads}
/>
## URL sync (shareable folder links)
Pass `url_sync: true` (or `url_sync: [id: "your-component-id"]`) to make
the embedded browser reflect the current folder / search / page / view
in the page URL — so a deep link like `…?folder=<uuid>` reopens that
folder on reload and can be shared with someone else.
use PhoenixKitWeb.Components.MediaBrowser.Embed, url_sync: true
# multiple browsers, or a non-default component id:
use PhoenixKitWeb.Components.MediaBrowser.Embed,
url_sync: [id: "my-media-browser"]
With `url_sync` on, the host template must pass the controlled-mode
attrs through to the component:
<.live_component
module={PhoenixKitWeb.Components.MediaBrowser}
id="media-browser"
on_navigate={:navigate}
initial_params={@initial_params}
parent_uploads={@uploads}
/>
Everything else is automatic: `initial_params` is parsed from the URL
in `on_mount`, folder navigation `push_patch`es the new params into the
address bar, and a reload feeds them back to the component. The folder
is tracked by **uuid** (stable across renames); an unknown / out-of-scope
uuid falls back to root. The base path is taken from the live URL, so
any router prefix is respected.
URL sync is implemented with LiveView lifecycle hooks
(`attach_hook(:handle_params)` + `attach_hook(:handle_info)` in
`on_mount`), **not** injected `handle_params/3` clauses — so it composes
cleanly with a host LiveView that already defines its own
`handle_params` / `handle_info` (e.g. an `…/orders/:id/edit/files`
page that loads the order in its own `handle_params`). Nothing to
reconcile; both run. The `push_patch` only appends the query string to
the *current* path, so every existing segment (locale, parent resource
ids, sub-tab) is preserved.
Single-browser-per-page is assumed: the query keys (`folder`, `q`,
`page`, `orphaned`, `view`) are not namespaced per component, so two
url-synced browsers on one page would fight over them. Give only one
the `url_sync` option in that case.
## What gets injected
* `on_mount` — calls `MediaBrowser.setup_uploads/1` so `@uploads.media_files`
is available on every mount of this LiveView. With `url_sync`, also
assigns `:initial_params` parsed from the mount params.
* Fallback `handle_event("validate", _, socket)` — absorbs the upload
channel's `phx-change` events. User-defined clauses with other event
names still win because they are defined first.
* With `url_sync`: a `:handle_params` hook (feeds URL params to the
component + captures the live path) and a `:handle_info` hook that
intercepts `{MediaBrowser, id, {:navigate, _}}` and `push_patch`es,
both attached in `on_mount` so they compose with the host's own
handlers.
* Fallback `handle_info({MediaBrowser, _, _}, socket)` — forwards to
`MediaBrowser.handle_parent_info/2` for component registration and
upload piping.
* Fallback `handle_info({:leaf_changed, _}, socket)` — routes
Leaf editor content updates from the sidebar comments (when
PhoenixKitComments is installed) to
`PhoenixKitComments.Web.CommentsComponent.forward_leaf_event/2`.
Without this, comment Leaf editors render but the typed
content never reaches the server. The clause only injects
when `PhoenixKitComments.Web.CommentsComponent` is loaded —
otherwise it's compiled away. User-defined `:leaf_changed`
clauses (e.g. for a post-content editor on the same page)
still win because they are defined first.
All fallbacks are injected via `@before_compile`, so user clauses
declared earlier in the module match before them.
"""
alias PhoenixKitWeb.Components.MediaBrowser
# ── on_mount ────────────────────────────────────────────────────
# `:default` — uploads only (the no-url_sync path; unchanged).
def on_mount(:default, _params, _session, socket) do
{:cont, MediaBrowser.setup_uploads(socket)}
end
# `{:embed, %{...}}` — url_sync on: parse the URL params into
# :initial_params so the component opens the shared folder on first render
# (controlled mode reads @initial_params), and seed the nav baseline so the
# :handle_params hook can skip redundant send_updates (see below). The
# no-url_sync path uses `:default` above — `__using__` never emits
# `{:embed, false}`, so there is no clause for it.
def on_mount({:embed, %{id: component_id}}, params, _session, socket) do
# `:initial_params` gives the component the right folder on the very
# first (even static) render — avoids a flash of root before the
# handle_params hook fires on connect. `:__phoenix_kit_mb_nav__` records
# the params already reflected in the component so the hook can skip a
# send_update when the next handle_params carries the same nav.
nav = parse_nav_params(params)
socket =
socket
|> MediaBrowser.setup_uploads()
|> Phoenix.Component.assign(:initial_params, nav)
|> Phoenix.Component.assign(:__phoenix_kit_mb_nav__, nav)
|> attach_url_sync_hooks(component_id)
{:cont, socket}
end
# URL-sync via lifecycle hooks rather than injected handle_params /
# handle_info clauses, so it composes with a host LiveView that already
# defines its own (e.g. an `…/orders/:id/edit/files` page that loads the
# order in its own handle_params). The :handle_params hook feeds URL
# params to the component and captures the live path; the :handle_info
# hook intercepts the component's {:navigate, …} and push_patches the
# new query onto that same path (preserving every path segment — locale,
# parent resource ids, sub-tab), {:halt}ing so it doesn't fall through.
#
# The :handle_params hook fires on *every* navigation in the host LiveView,
# not just media-browser ones, so it only send_updates when the parsed nav
# actually changed — otherwise an unrelated host push_patch would make the
# component re-run its folder/file/orphan/trash queries for no reason.
defp attach_url_sync_hooks(socket, component_id) do
socket
|> Phoenix.LiveView.attach_hook(:phoenix_kit_mb_url_sync_params, :handle_params, fn
params, uri, socket ->
socket = Phoenix.Component.assign(socket, :__phoenix_kit_mb_path__, URI.parse(uri).path)
nav = parse_nav_params(params)
if Phoenix.LiveView.connected?(socket) and nav != socket.assigns[:__phoenix_kit_mb_nav__] do
Phoenix.LiveView.send_update(MediaBrowser, id: component_id, nav_params: nav)
{:cont, Phoenix.Component.assign(socket, :__phoenix_kit_mb_nav__, nav)}
else
{:cont, socket}
end
end)
|> Phoenix.LiveView.attach_hook(:phoenix_kit_mb_url_sync_info, :handle_info, fn
{MediaBrowser, ^component_id, {:navigate, nav}}, socket ->
qs = build_nav_query(nav)
base = socket.assigns[:__phoenix_kit_mb_path__] || "/"
url = if qs == %{}, do: base, else: base <> "?" <> URI.encode_query(qs)
{:halt, Phoenix.LiveView.push_patch(socket, to: url)}
_msg, socket ->
{:cont, socket}
end)
end
# ── Shared URL <-> nav-params helpers (public so a host with its own
# handle_params can reuse the exact parse/build the macro uses) ──────
@doc """
Parse a LiveView params map into the `nav_params` shape MediaBrowser's
controlled mode expects (`folder`, `q`, `page`, `filter_orphaned`,
`view`).
"""
def parse_nav_params(params) do
%{
folder: params["folder"],
q: params["q"] || "",
page: parse_page(params["page"]),
filter_orphaned: params["orphaned"] == "1",
view: params["view"]
}
end
@doc """
Build the query-string map from a `{:navigate, params}` payload —
omitting defaults so a root, unsearched, first-page view yields a clean
bare URL.
"""
def build_nav_query(p) do
folder = p[:folder]
q = p[:q] || ""
page = p[:page] || 1
filter_orphaned = p[:filter_orphaned] || false
view = p[:view]
%{}
|> then(&if(folder, do: Map.put(&1, "folder", folder), else: &1))
|> then(&if(q != "", do: Map.put(&1, "q", q), else: &1))
|> then(&if(page > 1, do: Map.put(&1, "page", page), else: &1))
|> then(&if(filter_orphaned, do: Map.put(&1, "orphaned", "1"), else: &1))
|> then(&if(view == "all", do: Map.put(&1, "view", "all"), else: &1))
end
defp parse_page(p) do
case Integer.parse(p || "1") do
{n, _} when n > 0 -> n
_ -> 1
end
end
defmacro __using__(opts) do
sync =
case Keyword.get(opts, :url_sync, false) do
false -> false
true -> %{id: "media-browser"}
kw when is_list(kw) -> %{id: Keyword.get(kw, :id, "media-browser")}
end
on_mount_arg = if sync, do: {:embed, sync}, else: :default
quote do
@phoenix_kit_mb_url_sync unquote(sync != false)
on_mount({PhoenixKitWeb.Components.MediaBrowser.Embed, unquote(Macro.escape(on_mount_arg))})
@before_compile PhoenixKitWeb.Components.MediaBrowser.Embed
end
end
defmacro __before_compile__(env) do
# `url_sync` push_patches from the :handle_info hook. LiveView routes a
# push_patch issued inside handle_info through
# `sync_handle_params_with_live_redirect`, which calls
# `call_handle_params!/4` with `exported?` defaulting to TRUE — i.e. it
# invokes `view.handle_params/3` unconditionally. The :handle_params
# hook already did the real work, but if the host LiveView defines no
# handle_params/3 of its own that call blows up
# (UndefinedFunctionError). Inject a trivial stub in exactly that case;
# a host that defines its own keeps it (the hook composes alongside).
stub_handle_params? =
Module.get_attribute(env.module, :phoenix_kit_mb_url_sync, false) and
not Module.defines?(env.module, {:handle_params, 3})
handle_params_stub =
if stub_handle_params? do
quote do
def handle_params(_params, _uri, socket), do: {:noreply, socket}
end
else
quote do
end
end
# Fully-qualified references on purpose: this code is injected into the
# caller's module, where aliasing from Embed wouldn't be in scope.
#
# The leaf-forwarder is always injected (not gated at MACRO-compile
# time on Code.ensure_loaded?) because phoenix_kit_comments may not be
# loaded yet when phoenix_kit compiles — they're sibling deps with no
# compile-order guarantee. Runtime `Code.ensure_loaded?` inside the
# clause does the right thing: when the comments package is installed,
# the event is forwarded; when it isn't, the clause is a no-op.
quote do
def handle_event("validate", _params, socket), do: {:noreply, socket}
unquote(handle_params_stub)
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
def handle_info(
{PhoenixKitWeb.Components.MediaBrowser, _, _} = msg,
socket
) do
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
PhoenixKitWeb.Components.MediaBrowser.handle_parent_info(msg, socket)
end
def handle_info({:leaf_changed, _} = msg, socket) do
# credo:disable-for-next-line Credo.Check.Design.AliasUsage
PhoenixKitWeb.CommentsForwarding.forward_leaf_changed(msg, socket)
end
end
end
end