diff --git a/assets/css/components.css b/assets/css/components.css index 9cf8dbc9d..3cbba8d87 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -196,17 +196,20 @@ /* Toggleable menu */ .menu { - @apply absolute right-0 z-30 rounded-lg bg-white flex flex-col py-2 mt-1; + @apply absolute z-30 rounded-lg bg-white flex flex-col py-2 mt-1; box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15); } + .menu.right { + @apply right-0; + } + .menu.left { - right: auto; @apply left-0; } - .menu__item { - @apply flex space-x-3 px-5 py-2 items-center hover:bg-gray-50 whitespace-nowrap; + .menu-item { + @apply w-full flex space-x-3 px-5 py-2 items-center hover:bg-gray-50 whitespace-nowrap; } /* Boxes */ diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 58aad3efe..3c172c10e 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -9,10 +9,6 @@ solely client-side operations. /* === Global === */ -[data-element="menu"]:not([data-js-open]) > [data-content] { - @apply hidden; -} - [phx-hook="Highlight"][data-highlighted] > [data-source] { @apply hidden; } diff --git a/assets/js/app.js b/assets/js/app.js index 2d7e8b825..0d42a7184 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -17,7 +17,6 @@ import Session from "./session"; import FocusOnUpdate from "./focus_on_update"; import ScrollOnUpdate from "./scroll_on_update"; import VirtualizedLines from "./virtualized_lines"; -import Menu from "./menu"; import UserForm from "./user_form"; import VegaLite from "./vega_lite"; import Timer from "./timer"; @@ -36,7 +35,6 @@ const hooks = { FocusOnUpdate, ScrollOnUpdate, VirtualizedLines, - Menu, UserForm, VegaLite, Timer, @@ -89,3 +87,14 @@ window.addEventListener("lb:focus", (event) => { window.addEventListener("lb:set_value", (event) => { event.target.value = event.detail.value; }); + +// Other global handlers + +window.addEventListener("contextmenu", (event) => { + const target = event.target.closest("[data-contextmenu-trigger-click]"); + + if (target) { + event.preventDefault(); + target.dispatchEvent(new Event("click", { bubbles: true })); + } +}); diff --git a/assets/js/menu/index.js b/assets/js/menu/index.js deleted file mode 100644 index ecc18b245..000000000 --- a/assets/js/menu/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import { getAttributeOrDefault, parseBoolean } from "../lib/attribute"; - -/** - * A hook controlling a toggleable menu. - * - * Configuration: - * - * * `data-primary` - a boolean indicating whether to open on the primary - * click or the secondary click (like right mouse button click). Defaults to `true`. - * - * The element should have two children: - * - * * one annotated with `data-toggle` being a clickable element - * - * * one annotated with `data-content` with menu content - */ -const Menu = { - mounted() { - this.props = getProps(this); - - const toggleElement = this.el.querySelector("[data-toggle]"); - - if (!toggleElement) { - throw new Error("Menu must have a child with data-toggle attribute"); - } - - const contentElement = this.el.querySelector("[data-content]"); - - if (!contentElement) { - throw new Error("Menu must have a child with data-content attribute"); - } - - if (this.props.primary) { - toggleElement.addEventListener("click", (event) => { - if (this.el.hasAttribute("data-js-open")) { - this.el.removeAttribute("data-js-open"); - } else { - this.el.setAttribute("data-js-open", "true"); - // Postpone callback registration until the current click finishes bubbling. - setTimeout(() => { - document.addEventListener( - "click", - (event) => { - this.el.removeAttribute("data-js-open"); - }, - { once: true } - ); - }, 0); - } - }); - } else { - toggleElement.addEventListener("contextmenu", (event) => { - event.preventDefault(); - - if (this.el.hasAttribute("data-js-open")) { - this.el.removeAttribute("data-js-open"); - } else { - this.el.setAttribute("data-js-open", "true"); - // Postpone callback registration until the current click finishes bubbling. - setTimeout(() => { - const handler = (event) => { - this.el.removeAttribute("data-js-open"); - document.removeEventListener("click", handler); - document.removeEventListener("contextmenu", handler); - }; - - document.addEventListener("click", handler); - document.addEventListener("contextmenu", handler); - }, 0); - } - }); - } - }, -}; - -function getProps(hook) { - return { - primary: getAttributeOrDefault( - hook.el, - "data-primary", - "true", - parseBoolean - ), - }; -} - -export default Menu; diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index ffab6393c..4c44cb2e5 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -273,9 +273,9 @@ defmodule LivebookWeb.Helpers do ## Examples - <.with_password_toggle id="input-id"> - - + <.with_password_toggle id="input-id"> + + """ def with_password_toggle(assigns) do ~H""" @@ -293,6 +293,55 @@ defmodule LivebookWeb.Helpers do """ end + @doc """ + Renders a popup menu that shows up on toggle click. + + ## Assigns + + * `:id` - unique HTML id + + * `:disabled` - whether the menu is active. Defaults to `false` + + * `:position` - which side of the clickable the menu menu should + be attached to, either `"left"` or `"right"`. Defaults to `"right"` + + * `:secondary_click` - whether secondary click (usually right mouse click) + should open the menu. Defaults to `false` + + ## Examples + + <.menu id="my-menu"> + <:toggle> + + + <:content> + + + + """ + def menu(assigns) do + assigns = + assigns + |> assign_new(:disabled, fn -> false end) + |> assign_new(:position, fn -> "right" end) + |> assign_new(:secondary_click, fn -> false end) + + ~H""" +
+
+ <%= render_slot(@toggle) %> +
+
+ <%= render_slot(@content) %> +
+
+ """ + end + defdelegate ansi_string_to_html(string), to: LivebookWeb.Helpers.ANSI defdelegate ansi_string_to_html_lines(string), to: LivebookWeb.Helpers.ANSI diff --git a/lib/livebook_web/live/file_select_component.ex b/lib/livebook_web/live/file_select_component.ex index ff74a1130..9f78c326f 100644 --- a/lib/livebook_web/live/file_select_component.ex +++ b/lib/livebook_web/live/file_select_component.ex @@ -84,17 +84,19 @@ defmodule LivebookWeb.FileSelectComponent do autocomplete="off" /> -
- - -
+ + <%= if @inner_block do %>
<%= render_slot(@inner_block) %> @@ -189,19 +191,21 @@ defmodule LivebookWeb.FileSelectComponent do defp file_system_menu_button(assigns) do ~H""" -
- - -
+ + """ end @@ -258,40 +262,38 @@ defmodule LivebookWeb.FileSelectComponent do assigns = assign(assigns, :icon, icon) ~H""" -
- - -
+ + """ end diff --git a/lib/livebook_web/live/home_live/session_list_component.ex b/lib/livebook_web/live/home_live/session_list_component.ex index 3062bfabc..78aefa999 100644 --- a/lib/livebook_web/live/home_live/session_list_component.ex +++ b/lib/livebook_web/live/home_live/session_list_component.ex @@ -28,21 +28,23 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do

Running sessions (<%= length(@sessions) %>)

-
- - -
+ +
<.session_list sessions={@sessions} socket={@socket} /> @@ -81,30 +83,32 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do Created <%= format_creation_date(session.created_at) %> -
- - -
+ + <% end %> diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 50ae15d3b..8cc9d12bb 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -131,39 +131,41 @@ defmodule LivebookWeb.SessionLive do phx-blur="set_notebook_name" phx-hook="ContentEditable" data-update-attribute="phx-value-name"><%= @data_view.notebook_name %> -
- - -
+ +
<%= if @data_view.section_views == [] do %> diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex index 829a91d01..f50f591cd 100644 --- a/lib/livebook_web/live/session_live/section_component.ex +++ b/lib/livebook_web/live/session_live/section_component.ex @@ -25,25 +25,26 @@ defmodule LivebookWeb.SessionLive.SectionComponent do <%= if @section_view.valid_parents != [] and not @section_view.has_children? do %> -
- - - - -
+ + <% end %>