mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-22 03:16:16 +08:00
Implement menu with JS commands and extract into component (#698)
This commit is contained in:
parent
a29abbf87c
commit
3ed5da0106
9 changed files with 169 additions and 190 deletions
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -273,9 +273,9 @@ defmodule LivebookWeb.Helpers do
|
|||
|
||||
## Examples
|
||||
|
||||
<.with_password_toggle id="input-id">
|
||||
<input type="password" ...>
|
||||
</.with_password_toggle>
|
||||
<.with_password_toggle id="input-id">
|
||||
<input type="password" ...>
|
||||
</.with_password_toggle>
|
||||
"""
|
||||
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>
|
||||
<button>Open</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button class"menu-item">Option 1</button>
|
||||
</:content>
|
||||
</.menu>
|
||||
"""
|
||||
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"""
|
||||
<div class="relative"
|
||||
id={@id}>
|
||||
<div
|
||||
phx-click={not @disabled && JS.toggle(to: "##{@id}-content")}
|
||||
phx-click-away={JS.hide(to: "##{@id}-content")}
|
||||
data-contextmenu-trigger-click={@secondary_click}>
|
||||
<%= render_slot(@toggle) %>
|
||||
</div>
|
||||
<div id={"#{@id}-content"} class={"hidden menu #{@position}"}>
|
||||
<%= render_slot(@content) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defdelegate ansi_string_to_html(string), to: LivebookWeb.Helpers.ANSI
|
||||
defdelegate ansi_string_to_html_lines(string), to: LivebookWeb.Helpers.ANSI
|
||||
|
||||
|
|
|
@ -84,17 +84,19 @@ defmodule LivebookWeb.FileSelectComponent do
|
|||
autocomplete="off" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="relative" id="path-selector-menu" phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle tabindex="-1">
|
||||
<.remix_icon icon="add-line" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500" phx-click={js_show_new_dir_section()}>
|
||||
<.menu id="path-selector-menu">
|
||||
<:toggle>
|
||||
<button class="icon-button" tabindex="-1">
|
||||
<.remix_icon icon="add-line" class="text-xl" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button class="menu-item text-gray-500" phx-click={js_show_new_dir_section()}>
|
||||
<.remix_icon icon="folder-add-fill" class="text-gray-400" />
|
||||
<span class="font-medium">New directory</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
<%= if @inner_block do %>
|
||||
<div>
|
||||
<%= render_slot(@inner_block) %>
|
||||
|
@ -189,19 +191,21 @@ defmodule LivebookWeb.FileSelectComponent do
|
|||
|
||||
defp file_system_menu_button(assigns) do
|
||||
~H"""
|
||||
<div class="relative" id="file-system-menu" phx-hook="Menu" data-element="menu">
|
||||
<button type="button" class="button button-gray button-square-icon" data-toggle disabled={@file_system_select_disabled}>
|
||||
<.file_system_icon file_system={@file.file_system} />
|
||||
</button>
|
||||
<div class="menu left" data-content>
|
||||
<.menu id="file-system-menu" disabled={@file_system_select_disabled} position="left">
|
||||
<:toggle>
|
||||
<button type="button" class="button button-gray button-square-icon" disabled={@file_system_select_disabled}>
|
||||
<.file_system_icon file_system={@file.file_system} />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for {file_system, index} <- @file_systems |> Enum.with_index() do %>
|
||||
<%= if file_system == @file.file_system do %>
|
||||
<button class="menu__item text-gray-900">
|
||||
<button class="menu-item text-gray-900">
|
||||
<.file_system_icon file_system={file_system} />
|
||||
<span class="font-medium"><%= file_system_label(file_system) %></span>
|
||||
</button>
|
||||
<% else %>
|
||||
<button class="menu__item text-gray-500"
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-target={@myself}
|
||||
phx-click="set_file_system"
|
||||
phx-value-index={index}>
|
||||
|
@ -211,12 +215,12 @@ defmodule LivebookWeb.FileSelectComponent do
|
|||
<% end %>
|
||||
<% end %>
|
||||
<%= live_patch to: Routes.settings_path(@socket, :page),
|
||||
class: "menu__item text-gray-500 border-t border-gray-200" do %>
|
||||
class: "menu-item text-gray-500 border-t border-gray-200" do %>
|
||||
<.remix_icon icon="settings-3-line" />
|
||||
<span class="font-medium">Configure</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
"""
|
||||
end
|
||||
|
||||
|
@ -258,40 +262,38 @@ defmodule LivebookWeb.FileSelectComponent do
|
|||
assigns = assign(assigns, :icon, icon)
|
||||
|
||||
~H"""
|
||||
<div class="relative"
|
||||
id={"file-menu-#{@file_info.file.path}"}
|
||||
phx-hook="Menu"
|
||||
data-primary="false"
|
||||
data-element="menu">
|
||||
<button class="w-full flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
|
||||
data-toggle
|
||||
phx-click="set_path"
|
||||
phx-value-path={@file_info.file.path}
|
||||
phx-target={@myself}>
|
||||
<span class="block">
|
||||
<.remix_icon icon={@icon} class={"text-xl align-middle #{if(@file_info.is_running, do: "text-green-300", else: "text-gray-400")}"} />
|
||||
</span>
|
||||
<span class={"flex font-medium overflow-hidden whitespace-nowrap #{if(@file_info.is_running, do: "text-green-300", else: "text-gray-500")}"}>
|
||||
<%= if @file_info.highlighted != "" do %>
|
||||
<span class={"font-medium #{if(@file_info.is_running, do: "text-green-400", else: "text-gray-900")}"}>
|
||||
<%= @file_info.highlighted %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="overflow-hidden overflow-ellipsis">
|
||||
<%= @file_info.unhighlighted %>
|
||||
<.menu id={"file-#{Base.encode16 @file_info.file.path}"} secondary_click>
|
||||
<:toggle>
|
||||
<button class="w-full flex space-x-2 items-center p-2 rounded-lg hover:bg-gray-100 focus:ring-1 focus:ring-gray-400"
|
||||
data-toggle
|
||||
phx-click="set_path"
|
||||
phx-value-path={@file_info.file.path}
|
||||
phx-target={@myself}>
|
||||
<span class="block">
|
||||
<.remix_icon icon={@icon} class={"text-xl align-middle #{if(@file_info.is_running, do: "text-green-300", else: "text-gray-400")}"} />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<span class={"flex font-medium overflow-hidden whitespace-nowrap #{if(@file_info.is_running, do: "text-green-300", else: "text-gray-500")}"}>
|
||||
<%= if @file_info.highlighted != "" do %>
|
||||
<span class={"font-medium #{if(@file_info.is_running, do: "text-green-400", else: "text-gray-900")}"}>
|
||||
<%= @file_info.highlighted %>
|
||||
</span>
|
||||
<% end %>
|
||||
<span class="overflow-hidden overflow-ellipsis">
|
||||
<%= @file_info.unhighlighted %>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= if @file_info.editable do %>
|
||||
<button class="menu__item text-gray-500"
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-click="rename_file"
|
||||
phx-target={@myself}
|
||||
phx-value-path={@file_info.file.path}>
|
||||
<.remix_icon icon="edit-line" />
|
||||
<span class="font-medium">Rename</span>
|
||||
</button>
|
||||
<button class="menu__item text-red-600"
|
||||
<button class="menu-item text-red-600"
|
||||
phx-click="delete_file"
|
||||
phx-target={@myself}
|
||||
phx-value-path={@file_info.file.path}>
|
||||
|
@ -299,8 +301,8 @@ defmodule LivebookWeb.FileSelectComponent do
|
|||
<span class="font-medium">Delete</span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
@ -28,21 +28,23 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
<h2 class="mb-4 uppercase font-semibold text-gray-500">
|
||||
Running sessions (<%= length(@sessions) %>)
|
||||
</h2>
|
||||
<div class="relative" id={"sessions-order-menu"} phx-hook="Menu" data-element="menu">
|
||||
<button class="button button-outlined-gray px-4 py-1" data-toggle>
|
||||
<span><%= order_by_label(@order_by) %></span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<.menu id="sessions-order-menu">
|
||||
<:toggle>
|
||||
<button class="button button-outlined-gray px-4 py-1">
|
||||
<span><%= order_by_label(@order_by) %></span>
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none align-middle ml-1" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for order_by <- ["date", "title"] do %>
|
||||
<button class={"menu__item #{if order_by == @order_by, do: "text-gray-900", else: "text-gray-500"}"}
|
||||
<button class={"menu-item #{if order_by == @order_by, do: "text-gray-900", else: "text-gray-500"}"}
|
||||
phx-click={JS.push("set_order", value: %{order_by: order_by}, target: @myself)}>
|
||||
<.remix_icon icon={order_by_icon(order_by)} />
|
||||
<span class="font-medium"><%= order_by_label(order_by) %></span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
<.session_list sessions={@sessions} socket={@socket} />
|
||||
</div>
|
||||
|
@ -81,30 +83,32 @@ defmodule LivebookWeb.HomeLive.SessionListComponent do
|
|||
Created <%= format_creation_date(session.created_at) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" id={"session-#{session.id}-menu"} phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<button class="menu__item text-gray-500"
|
||||
<.menu id={"session-#{session.id}-menu"}>
|
||||
<:toggle>
|
||||
<button class="icon-button">
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-click="fork_session"
|
||||
phx-value-id={session.id}>
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<a class="menu__item text-gray-500"
|
||||
<a class="menu-item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, session.pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, session.id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
class: "menu-item text-red-600" do %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -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 %></h1>
|
||||
<div class="relative" id="session-menu" phx-hook="Menu" data-element="menu">
|
||||
<button class="icon-button" data-toggle>
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
<div class="menu" data-content>
|
||||
<.menu id="session-menu">
|
||||
<:toggle>
|
||||
<button class="icon-button">
|
||||
<.remix_icon icon="more-2-fill" class="text-xl" />
|
||||
</button>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= live_patch to: Routes.session_path(@socket, :export, @session.id, "livemd"),
|
||||
class: "menu__item text-gray-500" do %>
|
||||
class: "menu-item text-gray-500" do %>
|
||||
<.remix_icon icon="download-2-line" />
|
||||
<span class="font-medium">Export</span>
|
||||
<% end %>
|
||||
<button class="text-gray-500 menu__item"
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-click="erase_outputs">
|
||||
<.remix_icon icon="eraser-fill" />
|
||||
<span class="font-medium">Erase outputs</span>
|
||||
</button>
|
||||
<button class="text-gray-500 menu__item"
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-click="fork_session">
|
||||
<.remix_icon icon="git-branch-line" />
|
||||
<span class="font-medium">Fork</span>
|
||||
</button>
|
||||
<a class="text-gray-500 menu__item"
|
||||
<a class="menu-item text-gray-500"
|
||||
href={live_dashboard_process_path(@socket, @session.pid)}
|
||||
target="_blank">
|
||||
<.remix_icon icon="dashboard-2-line" />
|
||||
<span class="font-medium">See on Dashboard</span>
|
||||
</a>
|
||||
<%= live_patch to: Routes.home_path(@socket, :close_session, @session.id),
|
||||
class: "menu__item text-red-600" do %>
|
||||
class: "menu-item text-red-600" do %>
|
||||
<.remix_icon icon="close-circle-line" />
|
||||
<span class="font-medium">Close</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
</div>
|
||||
<div class="flex flex-col w-full space-y-16">
|
||||
<%= if @data_view.section_views == [] do %>
|
||||
|
|
|
@ -25,25 +25,26 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
</a>
|
||||
</span>
|
||||
<%= if @section_view.valid_parents != [] and not @section_view.has_children? do %>
|
||||
<div class="relative" id={"section-#{@section_view.id}-branch-menu"} phx-hook="Menu" data-element="menu">
|
||||
<span class="tooltip top" data-tooltip="Branch out from">
|
||||
<button class="icon-button"
|
||||
aria-label="branch out from other section"
|
||||
data-toggle>
|
||||
<.remix_icon icon="git-branch-line" class="text-xl flip-horizontally" />
|
||||
</button>
|
||||
</span>
|
||||
<div class="menu" data-content>
|
||||
<.menu id={"section-#{@section_view.id}-branch-menu"}>
|
||||
<:toggle>
|
||||
<span class="tooltip top" data-tooltip="Branch out from">
|
||||
<button class="icon-button"
|
||||
aria-label="branch out from other section">
|
||||
<.remix_icon icon="git-branch-line" class="text-xl flip-horizontally" />
|
||||
</button>
|
||||
</span>
|
||||
</:toggle>
|
||||
<:content>
|
||||
<%= for parent <- @section_view.valid_parents do %>
|
||||
<%= if @section_view.parent && @section_view.parent.id == parent.id do %>
|
||||
<button class="menu__item text-gray-900"
|
||||
<button class="menu-item text-gray-900"
|
||||
phx-click="unset_section_parent"
|
||||
phx-value-section_id={@section_view.id}>
|
||||
<.remix_icon icon="arrow-right-s-line" />
|
||||
<span class="font-medium"><%= parent.name %></span>
|
||||
</button>
|
||||
<% else %>
|
||||
<button class="menu__item text-gray-500"
|
||||
<button class="menu-item text-gray-500"
|
||||
phx-click="set_section_parent"
|
||||
phx-value-section_id={@section_view.id}
|
||||
phx-value-parent_id={parent.id}>
|
||||
|
@ -52,8 +53,8 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</.menu>
|
||||
<% end %>
|
||||
<span class="tooltip top" data-tooltip="Move up">
|
||||
<button class="icon-button"
|
||||
|
|
Loading…
Add table
Reference in a new issue