Add copy to clipboard button to virtualized output (#393)

* Add copy to clipboard button to virtualized output

* Move text output into its own component

* Update button background
This commit is contained in:
Jonatan Kłosko 2021-06-24 12:15:12 +02:00 committed by GitHub
parent cdf2acf121
commit 6c53c09a61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 37 deletions

View file

@ -54,11 +54,11 @@
}
.icon-button {
@apply p-1 flex items-center justify-center text-gray-400 hover:text-gray-800;
@apply p-1 flex items-center justify-center text-gray-400 hover:text-gray-800 rounded-full;
}
.icon-button:focus {
@apply rounded-full bg-gray-100;
@apply bg-gray-100;
}
.icon-button i {

View file

@ -121,3 +121,7 @@ solely client-side operations.
[data-element="clients-list-item"][data-js-followed] [data-meta="follow"] {
@apply hidden;
}
[phx-hook="VirtualizedLines"]:not(:hover) [data-clipboard] {
@apply hidden;
}

View file

@ -23,6 +23,8 @@ import { getLineHeight } from "../lib/utils";
*
* * one annotated with `data-content` where the visible elements are rendered,
* it should contain any styling relevant for the container
*
* Also a `data-clipboard` child button is used for triggering copy-to-clipboard.
*/
const VirtualizedLines = {
mounted() {
@ -62,6 +64,18 @@ const VirtualizedLines = {
this.state.contentElement,
config
);
this.el
.querySelector("[data-clipboard]")
.addEventListener("click", (event) => {
const content = Array.from(this.state.templateElement.children)
.map((child) => child.innerText)
.join("");
if ("clipboard" in navigator) {
navigator.clipboard.writeText(content);
}
});
},
updated() {

View file

@ -0,0 +1,29 @@
defmodule LivebookWeb.Output.TextComponent do
use LivebookWeb, :live_component
@impl true
def render(assigns) do
~L"""
<div id="virtualized-text-<%= @id %>"
class="relative"
phx-hook="VirtualizedLines"
data-max-height="300"
data-follow="<%= @follow %>">
<div data-template class="hidden">
<%= for line <- ansi_to_html_lines(@content) do %>
<%# Add a newline, so that multiple lines can be copied properly %>
<div><%= [line, "\n"] %></div>
<% end %>
</div>
<div data-content class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
id="virtualized-text-<%= @id %>-content"
phx-update="ignore"></div>
<div class="absolute right-4 top-0 z-10">
<button class="icon-button bg-gray-100" data-clipboard>
<%= remix_icon("clipboard-line", class: "text-lg") %>
</button>
</div>
</div>
"""
end
end

View file

@ -286,15 +286,22 @@ defmodule LivebookWeb.SessionLive.CellComponent do
end
defp render_output(_socket, text, id) when is_binary(text) do
text
# Captured output usually has a trailing newline that we can ignore,
# because each line is itself an HTML block anyway.
|> String.replace_suffix("\n", "")
|> render_virtualized_output(id, follow: true)
text = String.replace_suffix(text, "\n", "")
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true)
end
defp render_output(_socket, {:text, text}, id) do
render_virtualized_output(text, id)
live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false)
end
defp render_output(_socket, {:image, content, mime_type}, id) do
live_component(LivebookWeb.Output.ImageComponent,
id: id,
content: content,
mime_type: mime_type
)
end
defp render_output(_socket, {:vega_lite_static, spec}, id) do
@ -315,14 +322,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
)
end
defp render_output(_socket, {:image, content, mime_type}, id) do
live_component(LivebookWeb.Output.ImageComponent,
id: id,
content: content,
mime_type: mime_type
)
end
defp render_output(_socket, {:error, formatted}, _id) do
render_error_message_output(formatted)
end
@ -334,29 +333,6 @@ defmodule LivebookWeb.SessionLive.CellComponent do
""")
end
defp render_virtualized_output(text, id, opts \\ []) do
follow = Keyword.get(opts, :follow, false)
lines = ansi_to_html_lines(text)
assigns = %{lines: lines, id: id, follow: follow}
~L"""
<div id="<%= @id %>"
phx-hook="VirtualizedLines"
data-max-height="300"
data-follow="<%= follow %>">
<div data-template class="hidden">
<%= for line <- @lines do %>
<%# Add a newline, so that multiple lines can be copied properly %>
<div><%= [line, "\n"] %></div>
<% end %>
</div>
<div data-content class="overflow-auto whitespace-pre font-editor text-gray-500 tiny-scrollbar"
id="<%= @id %>-content"
phx-update="ignore"></div>
</div>
"""
end
defp render_error_message_output(message) do
assigns = %{message: message}