diff --git a/assets/js/lib/attribute.js b/assets/js/lib/attribute.js index ffb1f54aa..60da3a49f 100644 --- a/assets/js/lib/attribute.js +++ b/assets/js/lib/attribute.js @@ -10,6 +10,19 @@ export function getAttributeOrThrow(element, attr, transform = null) { return transform ? transform(value) : value; } +export function getAttributeOrDefault( + element, + attr, + defaultAttrVal, + transform = null +) { + const value = element.hasAttribute(attr) + ? element.getAttribute(attr) + : defaultAttrVal; + + return transform ? transform(value) : value; +} + export function parseBoolean(value) { if (value === "true") { return true; diff --git a/assets/js/menu/index.js b/assets/js/menu/index.js index e72bc6d4d..ecc18b245 100644 --- a/assets/js/menu/index.js +++ b/assets/js/menu/index.js @@ -1,6 +1,13 @@ +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 @@ -9,6 +16,8 @@ */ const Menu = { mounted() { + this.props = getProps(this); + const toggleElement = this.el.querySelector("[data-toggle]"); if (!toggleElement) { @@ -21,24 +30,58 @@ const Menu = { throw new Error("Menu must have a child with data-content attribute"); } - 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) => { + 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"); - }, - { once: true } - ); - }, 0); - } - }); + 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/live/path_select_component.ex b/lib/livebook_web/live/path_select_component.ex index 2633c21b7..23e75ca64 100644 --- a/lib/livebook_web/live/path_select_component.ex +++ b/lib/livebook_web/live/path_select_component.ex @@ -20,7 +20,16 @@ defmodule LivebookWeb.PathSelectComponent do @impl true def mount(socket) do inner_block = Map.get(socket.assigns, :inner_block, nil) - {:ok, assign(socket, inner_block: inner_block, current_dir: nil, new_directory_name: nil)} + + {:ok, + assign(socket, + inner_block: inner_block, + current_dir: nil, + new_directory_name: nil, + deleting_path: nil, + renaming_path: nil, + renamed_name: "" + )} end @impl true @@ -89,6 +98,27 @@ defmodule LivebookWeb.PathSelectComponent do <% end %> + <%= if @deleting_path do %> +
+ Are you sure you want to irreversibly delete + <%= @deleting_path %>? +
+