mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-11 15:34:37 +08:00
UI polishing (#43)
* Tiny UI improvements * Add icon for entering insert mode on a markdown cell * Highlight selected section * Improve contenteditable elements * Highlight notebook/section name while editing
This commit is contained in:
parent
0b77fd4279
commit
77675ad61e
10 changed files with 72 additions and 65 deletions
|
@ -1,6 +1,6 @@
|
||||||
import marked from "marked";
|
import marked from "marked";
|
||||||
import morphdom from "morphdom";
|
import morphdom from "morphdom";
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders markdown content in the given container.
|
* Renders markdown content in the given container.
|
||||||
|
|
|
@ -25,6 +25,13 @@ const ContentEditable = {
|
||||||
const text = event.clipboardData.getData("text/plain");
|
const text = event.clipboardData.getData("text/plain");
|
||||||
document.execCommand("insertText", false, text);
|
document.execCommand("insertText", false, text);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Unfocus the element on Enter or Escape
|
||||||
|
this.el.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter" || event.key === "Escape") {
|
||||||
|
this.el.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updated() {
|
updated() {
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
export function isMacOS() {
|
export function isMacOS() {
|
||||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEditableElement(element) {
|
||||||
|
return (
|
||||||
|
["input", "textarea"].includes(element.tagName.toLowerCase()) ||
|
||||||
|
element.contentEditable === "true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
|
import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
|
||||||
import { isMacOS } from "../lib/utils";
|
import { isMacOS, isEditableElement } from "../lib/utils";
|
||||||
import KeyBuffer from "./key_buffer";
|
import KeyBuffer from "./key_buffer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,6 +42,11 @@ const Session = {
|
||||||
this.pushEvent("queue_focused_cell_evaluation");
|
this.pushEvent("queue_focused_cell_evaluation");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (isEditableElement(event.target)) {
|
||||||
|
keyBuffer.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
keyBuffer.push(event.key);
|
keyBuffer.push(event.key);
|
||||||
|
|
||||||
if (keyBuffer.tryMatch(["d", "d"])) {
|
if (keyBuffer.tryMatch(["d", "d"])) {
|
||||||
|
|
|
@ -18,8 +18,13 @@ defmodule LiveBookWeb.CellComponent do
|
||||||
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
|
def render_cell_content(%{cell: %{type: :markdown}} = assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<%= if @focused do %>
|
<%= if @focused do %>
|
||||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
<div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10">
|
||||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
<%= unless @insert_mode do %>
|
||||||
|
<button phx-click="enable_insert_mode" class="text-gray-500 hover:text-current">
|
||||||
|
<%= Icons.svg(:pencil, class: "h-6") %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
<button phx-click="delete_focused_cell" class="text-gray-500 hover:text-current">
|
||||||
<%= Icons.svg(:trash, class: "h-6") %>
|
<%= Icons.svg(:trash, class: "h-6") %>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,11 +43,11 @@ defmodule LiveBookWeb.CellComponent do
|
||||||
def render_cell_content(%{cell: %{type: :elixir}} = assigns) do
|
def render_cell_content(%{cell: %{type: :elixir}} = assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<%= if @focused do %>
|
<%= if @focused do %>
|
||||||
<div class="flex flex-col items-center space-y-2 absolute right-0 top-0 -mr-10">
|
<div class="flex flex-col items-center space-y-2 absolute z-50 right-0 top-0 -mr-10">
|
||||||
<button phx-click="queue_cell_evaluation" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
<button phx-click="queue_focused_cell_evaluation" class="text-gray-500 hover:text-current">
|
||||||
<%= Icons.svg(:play, class: "h-6") %>
|
<%= Icons.svg(:play, class: "h-6") %>
|
||||||
</button>
|
</button>
|
||||||
<button phx-click="delete_cell" phx-value-cell_id="<%= @cell.id %>" class="text-gray-500 hover:text-current">
|
<button phx-click="delete_focused_cell" class="text-gray-500 hover:text-current">
|
||||||
<%= Icons.svg(:trash, class: "h-6") %>
|
<%= Icons.svg(:trash, class: "h-6") %>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,16 +7,6 @@ defmodule LiveBookWeb.Icons do
|
||||||
"""
|
"""
|
||||||
def svg(name, attrs \\ [])
|
def svg(name, attrs \\ [])
|
||||||
|
|
||||||
def svg(:chevron_right, attrs) do
|
|
||||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
|
||||||
|
|
||||||
~L"""
|
|
||||||
<%= tag(:svg, @attrs) %>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
def svg(:play, attrs) do
|
def svg(:play, attrs) do
|
||||||
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||||
|
|
||||||
|
@ -88,6 +78,16 @@ defmodule LiveBookWeb.Icons do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def svg(:pencil, attrs) do
|
||||||
|
assigns = %{attrs: heroicon_svg_attrs(attrs)}
|
||||||
|
|
||||||
|
~L"""
|
||||||
|
<%= tag(:svg, @attrs) %>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||||
|
</svg>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
# https://heroicons.com
|
# https://heroicons.com
|
||||||
defp heroicon_svg_attrs(attrs) do
|
defp heroicon_svg_attrs(attrs) do
|
||||||
heroicon_svg_attrs = [
|
heroicon_svg_attrs = [
|
||||||
|
|
|
@ -3,16 +3,16 @@ defmodule LiveBookWeb.InsertCellComponent do
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<div class="opacity-0 hover:opacity-100 flex space-x-2 justify-center items-center">
|
<div class="<%= if(@persistent, do: "opacity-100", else: "opacity-0") %> hover:opacity-100 flex space-x-2 justify-center items-center">
|
||||||
<%= line() %>
|
<%= line() %>
|
||||||
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-100 border border-gray-200 bg-gray-50"
|
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-50 border border-gray-200"
|
||||||
phx-click="insert_cell"
|
phx-click="insert_cell"
|
||||||
phx-value-type="markdown"
|
phx-value-type="markdown"
|
||||||
phx-value-section_id="<%= @section_id %>"
|
phx-value-section_id="<%= @section_id %>"
|
||||||
phx-value-index="<%= @index %>">
|
phx-value-index="<%= @index %>">
|
||||||
+ Markdown
|
+ Markdown
|
||||||
</button>
|
</button>
|
||||||
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-100 border border-gray-200 bg-gray-50"
|
<button class="py-1 px-2 rounded-md text-sm hover:bg-gray-50 border border-gray-200"
|
||||||
phx-click="insert_cell"
|
phx-click="insert_cell"
|
||||||
phx-value-type="elixir"
|
phx-value-type="elixir"
|
||||||
phx-value-section_id="<%= @section_id %>"
|
phx-value-section_id="<%= @section_id %>"
|
||||||
|
|
|
@ -4,10 +4,9 @@ defmodule LiveBookWeb.SectionComponent do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<div class="<%= if not @selected, do: "hidden" %>">
|
<div class="<%= if not @selected, do: "hidden" %>">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex space-x-4 items-center">
|
||||||
<div class="flex space-x-2 items-center text-gray-600">
|
<div class="flex flex-grow space-x-2 items-center text-gray-600">
|
||||||
<%= Icons.svg(:chevron_right, class: "h-8") %>
|
<h2 class="flex-grow text-gray-900 font-semibold text-3xl py-2 border-b-2 border-transparent hover:border-blue-100 focus:border-blue-300"
|
||||||
<h2 class="text-3xl"
|
|
||||||
id="section-<%= @section.id %>-name"
|
id="section-<%= @section.id %>-name"
|
||||||
contenteditable
|
contenteditable
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
|
@ -24,12 +23,13 @@ defmodule LiveBookWeb.SectionComponent do
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="container py-4">
|
<div class="container py-2">
|
||||||
<div class="flex flex-col space-y-2 pb-80">
|
<div class="flex flex-col space-y-2 pb-80">
|
||||||
<%= live_component @socket, LiveBookWeb.InsertCellComponent,
|
<%= live_component @socket, LiveBookWeb.InsertCellComponent,
|
||||||
id: "#{@section.id}:0",
|
id: "#{@section.id}:0",
|
||||||
section_id: @section.id,
|
section_id: @section.id,
|
||||||
index: 0 %>
|
index: 0,
|
||||||
|
persistent: @section.cells == [] %>
|
||||||
<%= for {cell, index} <- Enum.with_index(@section.cells) do %>
|
<%= for {cell, index} <- Enum.with_index(@section.cells) do %>
|
||||||
<%= live_component @socket, LiveBookWeb.CellComponent,
|
<%= live_component @socket, LiveBookWeb.CellComponent,
|
||||||
id: cell.id,
|
id: cell.id,
|
||||||
|
@ -40,7 +40,8 @@ defmodule LiveBookWeb.SectionComponent do
|
||||||
<%= live_component @socket, LiveBookWeb.InsertCellComponent,
|
<%= live_component @socket, LiveBookWeb.InsertCellComponent,
|
||||||
id: "#{@section.id}:#{index + 1}",
|
id: "#{@section.id}:#{index + 1}",
|
||||||
section_id: @section.id,
|
section_id: @section.id,
|
||||||
index: index + 1 %>
|
index: index + 1,
|
||||||
|
persistent: false %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,25 +70,25 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="flex flex-grow h-full"
|
<div class="flex flex-grow h-full"
|
||||||
id="session"
|
id="session"
|
||||||
phx-hook="Session"
|
phx-hook="Session"
|
||||||
data-insert-mode="<%= @insert_mode %>"
|
data-insert-mode="<%= @insert_mode %>"
|
||||||
data-focused-cell-id="<%= @focused_cell_id %>"
|
data-focused-cell-id="<%= @focused_cell_id %>"
|
||||||
data-focused-cell-type="<%= @focused_cell_type %>">
|
data-focused-cell-type="<%= @focused_cell_type %>">
|
||||||
<div class="flex flex-col w-1/5 bg-gray-100 border-r-2 border-gray-200">
|
<div class="flex flex-col w-1/5 bg-gray-100 border-r border-gray-200">
|
||||||
<h1 id="notebook-name"
|
<h1 class="m-6 py-1 text-2xl border-b-2 border-transparent hover:border-blue-100 focus:border-blue-300"
|
||||||
contenteditable
|
id="notebook-name"
|
||||||
spellcheck="false"
|
contenteditable
|
||||||
phx-blur="set_notebook_name"
|
spellcheck="false"
|
||||||
phx-hook="ContentEditable"
|
phx-blur="set_notebook_name"
|
||||||
data-update-attribute="phx-value-name"
|
phx-hook="ContentEditable"
|
||||||
class="p-8 text-2xl"><%= @data.notebook.name %></h1>
|
data-update-attribute="phx-value-name"><%= @data.notebook.name %></h1>
|
||||||
|
|
||||||
<div class="flex-grow flex flex-col space-y-2 pl-4">
|
<div class="flex-grow flex flex-col space-y-2 pl-4">
|
||||||
<%= for section <- @data.notebook.sections do %>
|
<%= for section <- @data.notebook.sections do %>
|
||||||
<div phx-click="select_section"
|
<div phx-click="select_section"
|
||||||
phx-value-section_id="<%= section.id %>"
|
phx-value-section_id="<%= section.id %>"
|
||||||
class="py-2 px-4 rounded-l-md cursor-pointer text-gray-500 hover:text-current">
|
class="py-2 px-4 rounded-l-md cursor-pointer hover:text-current border border-r-0 <%= if(section.id == @selected_section_id, do: "bg-white border-gray-200", else: "text-gray-500 border-transparent") %>">
|
||||||
<span>
|
<span>
|
||||||
<%= section.name %>
|
<%= section.name %>
|
||||||
</span>
|
</span>
|
||||||
|
@ -200,12 +200,6 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
|
||||||
Session.delete_cell(socket.assigns.session_id, cell_id)
|
|
||||||
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("delete_focused_cell", %{}, socket) do
|
def handle_event("delete_focused_cell", %{}, socket) do
|
||||||
if socket.assigns.focused_cell_id do
|
if socket.assigns.focused_cell_id do
|
||||||
Session.delete_cell(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
Session.delete_cell(socket.assigns.session_id, socket.assigns.focused_cell_id)
|
||||||
|
@ -271,10 +265,12 @@ defmodule LiveBookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id}, socket) do
|
def handle_event("enable_insert_mode", %{}, socket) do
|
||||||
Session.queue_cell_evaluation(socket.assigns.session_id, cell_id)
|
if socket.assigns.focused_cell_id do
|
||||||
|
{:noreply, assign(socket, insert_mode: true)}
|
||||||
{:noreply, socket}
|
else
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_focused_cell_evaluation", %{}, socket) do
|
def handle_event("queue_focused_cell_evaluation", %{}, socket) do
|
||||||
|
|
|
@ -92,20 +92,6 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
Session.get_data(session_id)
|
Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "queueing cell evaluation", %{conn: conn, session_id: session_id} do
|
|
||||||
section_id = insert_section(session_id)
|
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, "/sessions/#{session_id}")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element("#session")
|
|
||||||
|> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id})
|
|
||||||
|
|
||||||
assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} =
|
|
||||||
Session.get_data(session_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "queueing focused cell evaluation", %{conn: conn, session_id: session_id} do
|
test "queueing focused cell evaluation", %{conn: conn, session_id: session_id} do
|
||||||
section_id = insert_section(session_id)
|
section_id = insert_section(session_id)
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(10)")
|
||||||
|
|
Loading…
Add table
Reference in a new issue