diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index b57443c75..56bac12a4 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -113,14 +113,8 @@ solely client-side operations. [data-el-cell][data-js-amplified] [data-el-amplify-outputs-button] - [data-el-zoom-in-icon] { - @apply hidden; -} - -[data-el-cell]:not([data-js-amplified]) - [data-el-amplify-outputs-button] - [data-el-zoom-out-icon] { - @apply hidden; + .icon-button { + @apply bg-gray-100 text-gray-900; } [data-el-cell][data-type="smart"]:not([data-js-source-visible]) @@ -167,14 +161,10 @@ solely client-side operations. @apply hidden; } -[data-el-cell][data-type="smart"]:not([data-js-source-visible]) - [data-el-show-ui-icon] { - @apply hidden; -} - [data-el-cell][data-type="smart"][data-js-source-visible] - [data-el-show-code-icon] { - @apply hidden; + [data-el-toggle-source-button] + .icon-button { + @apply bg-gray-100 text-gray-900; } [data-el-cell][data-type="smart"][data-js-source-visible] diff --git a/assets/js/hooks/confirm_modal.js b/assets/js/hooks/confirm_modal.js index 4666e0ffc..705a69e24 100644 --- a/assets/js/hooks/confirm_modal.js +++ b/assets/js/hooks/confirm_modal.js @@ -26,6 +26,7 @@ const ConfirmModal = { confirm_text, confirm_icon, danger, + html, opt_out_id, } = event.detail; @@ -33,7 +34,13 @@ const ConfirmModal = { liveSocket.execJS(event.target, event.detail.on_confirm); } else { titleEl.textContent = title; - descriptionEl.textContent = description; + + if (html) { + descriptionEl.innerHTML = description; + } else { + descriptionEl.textContent = description; + } + confirmTextEl.textContent = confirm_text; if (confirm_icon) { diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index 1e1607fe6..b022021f6 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -76,6 +76,10 @@ const JSView = { window.addEventListener("message", this._handleWindowMessage); }); + this.hiddenInput = document.createElement("input"); + this.hiddenInput.style.display = "none"; + this.el.appendChild(this.hiddenInput); + this.loadIframe(); // Channel events @@ -283,7 +287,11 @@ const JSView = { // Replicate the child events on the current element, // so that they are detected upstream in the session hook const event = this.replicateDomEvent(message.event); - this.el.dispatchEvent(event); + if (message.isTargetEditable) { + this.hiddenInput.dispatchEvent(event); + } else { + this.el.dispatchEvent(event); + } } else if (message.type === "event") { const { event, payload } = message; const raw = transportEncode([event, this.props.ref], payload); diff --git a/assets/js/hooks/js_view/iframe.js b/assets/js/hooks/js_view/iframe.js index dadceb76a..41eb0497f 100644 --- a/assets/js/hooks/js_view/iframe.js +++ b/assets/js/hooks/js_view/iframe.js @@ -26,7 +26,7 @@ import { sha256Base64 } from "../../lib/utils"; // (2): https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox // (3): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts -const IFRAME_SHA256 = "+uJyGu0Ey7uVV7WwRwg7GyjwCkMNRBnyNc25iGFpYXc="; +const IFRAME_SHA256 = "4gyeA71Bpb4SGj2M0BUdT1jtk6wjUqOf6Q8wVYp7htc="; export function initializeIframeSource(iframe, iframePort) { const iframeUrl = getIframeUrl(iframePort); @@ -42,8 +42,8 @@ export function initializeIframeSource(iframe, iframePort) { function getIframeUrl(iframePort) { return window.location.protocol === "https:" - ? "https://livebook.space/iframe/v2.html" - : `http://${window.location.hostname}:${iframePort}/iframe/v2.html`; + ? "https://livebook.space/iframe/v3.html" + : `http://${window.location.hostname}:${iframePort}/iframe/v3.html`; } let iframeVerificationPromise = null; diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index b236e1270..5b9dda398 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -284,6 +284,23 @@ const Session = { const key = event.key; const keyBuffer = this.keyBuffer; + // Universal shortcuts + if (cmd && shift && !alt && key === "Enter") { + cancelEvent(event); + this.queueFullCellsEvaluation(true); + return; + } else if (cmd && !alt && key === "Enter") { + cancelEvent(event); + if (isEvaluable(this.focusedCellType())) { + this.queueFocusedCellEvaluation(); + } + return; + } else if (cmd && key === "s") { + cancelEvent(event); + this.saveNotebook(); + return; + } + if (this.insertMode) { keyBuffer.reset(); @@ -292,46 +309,23 @@ const Session = { if (!this.escapesMonacoWidget(event)) { this.escapeInsertMode(); } - } else if (cmd && shift && !alt && key === "Enter") { - cancelEvent(event); - this.queueFullCellsEvaluation(true); - } else if (cmd && !alt && key === "Enter") { - cancelEvent(event); - if (isEvaluable(this.focusedCellType())) { - this.queueFocusedCellEvaluation(); - } - } else if (cmd && key === "s") { - cancelEvent(event); - this.saveNotebook(); + } + // Ignore keystrokes on input fields + } else if (isEditableElement(event.target)) { + keyBuffer.reset(); + + // Use Escape for universal blur + if (key === "Escape") { + event.target.blur(); } } else { - // Ignore keystrokes on input fields - if (isEditableElement(event.target)) { - keyBuffer.reset(); - - // Use Escape for universal blur - if (key === "Escape") { - event.target.blur(); - } - - return; - } - keyBuffer.push(event.key); - if (cmd && key === "s") { - cancelEvent(event); - this.saveNotebook(); - } else if (keyBuffer.tryMatch(["d", "d"])) { + if (keyBuffer.tryMatch(["d", "d"])) { this.deleteFocusedCell(); - } else if (cmd && shift && !alt && key === "Enter") { - this.queueFullCellsEvaluation(true); } else if (keyBuffer.tryMatch(["e", "a"])) { this.queueFullCellsEvaluation(false); - } else if ( - keyBuffer.tryMatch(["e", "e"]) || - (cmd && !alt && key === "Enter") - ) { + } else if (keyBuffer.tryMatch(["e", "e"])) { if (isEvaluable(this.focusedCellType())) { this.queueFocusedCellEvaluation(); } diff --git a/iframe/priv/static/iframe/v3.html b/iframe/priv/static/iframe/v3.html new file mode 100644 index 000000000..faa5bdac4 --- /dev/null +++ b/iframe/priv/static/iframe/v3.html @@ -0,0 +1,205 @@ + + +
+#{text}
"
+
defp insert_smart_cell(js \\ %JS{}, definition, section_id, cell_id) do
JS.push(js, "insert_cell_below",
value: %{
diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex
index a68846468..eb8ab8065 100644
--- a/lib/livebook_web/live/session_live/section_component.ex
+++ b/lib/livebook_web/live/session_live/section_component.ex
@@ -110,6 +110,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
id={cell_view.id}
session_id={@session_id}
runtime={@runtime}
+ installing?={@installing?}
cell_view={cell_view} />
<.live_component module={LivebookWeb.SessionLive.InsertButtonsComponent}
id={"insert-buttons-#{@section_view.id}-#{index}"}
diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs
index 3f20af258..32e3bbc7a 100644
--- a/test/livebook/session/data_test.exs
+++ b/test/livebook/session/data_test.exs
@@ -3564,6 +3564,80 @@ defmodule Livebook.Session.DataTest do
end
end
+ describe "cell_ids_for_reevaluation/2" do
+ test "does not include the setup cell" do
+ data =
+ data_after_operations!([
+ {:insert_section, self(), 0, "s1"},
+ {:set_runtime, self(), connected_noop_runtime()},
+ evaluate_cells_operations(["setup"])
+ ])
+
+ assert Data.cell_ids_for_reevaluation(data) == []
+ end
+
+ test "includes evaluated cells" do
+ data =
+ data_after_operations!([
+ {:insert_section, self(), 0, "s1"},
+ {:insert_cell, self(), "s1", 0, :code, "c1", %{}},
+ {:insert_cell, self(), "s1", 1, :code, "c2", %{}},
+ {:set_runtime, self(), connected_noop_runtime()},
+ evaluate_cells_operations(["setup", "c1", "c2"])
+ ])
+
+ assert Data.cell_ids_for_reevaluation(data) |> Enum.sort() == ["c1", "c2"]
+ end
+
+ test "includes stale cells" do
+ data =
+ data_after_operations!([
+ {:insert_section, self(), 0, "s1"},
+ {:insert_cell, self(), "s1", 0, :code, "c1", %{}},
+ {:insert_cell, self(), "s1", 1, :code, "c2", %{}},
+ {:set_runtime, self(), connected_noop_runtime()},
+ evaluate_cells_operations(["setup", "c1", "c2"]),
+ # Reevaluate cell 1
+ evaluate_cells_operations(["c1"])
+ ])
+
+ assert Data.cell_ids_for_reevaluation(data) |> Enum.sort() == ["c1", "c2"]
+ end
+
+ test "stops reevaluation on the first fresh cell" do
+ data =
+ data_after_operations!([
+ {:insert_section, self(), 0, "s1"},
+ {:insert_cell, self(), "s1", 0, :code, "c1", %{}},
+ {:insert_cell, self(), "s1", 1, :code, "c2", %{}},
+ {:set_runtime, self(), connected_noop_runtime()},
+ evaluate_cells_operations(["setup", "c1", "c2"]),
+ # Reevaluate cell 1
+ {:insert_cell, self(), "s1", 1, :code, "c3", %{}}
+ ])
+
+ assert Data.cell_ids_for_reevaluation(data) |> Enum.sort() == ["c1"]
+ end
+
+ test "considers each branch separately" do
+ data =
+ data_after_operations!([
+ {:insert_section, self(), 0, "s1"},
+ {:insert_cell, self(), "s1", 0, :code, "c1", %{}},
+ {:insert_section, self(), 1, "s2"},
+ {:insert_cell, self(), "s2", 0, :code, "c2", %{}},
+ {:insert_cell, self(), "s2", 1, :code, "c3", %{}},
+ {:insert_section, self(), 2, "s3"},
+ {:insert_cell, self(), "s3", 0, :code, "c4", %{}},
+ {:set_section_parent, self(), "s2", "s1"},
+ {:set_runtime, self(), connected_noop_runtime()},
+ evaluate_cells_operations(["setup", "c1", "c2", "c4"])
+ ])
+
+ assert Data.cell_ids_for_reevaluation(data) |> Enum.sort() == ["c1", "c2", "c4"]
+ end
+ end
+
defp evaluate_cells_operations(cell_ids) do
[
{:queue_cells_evaluation, self(), cell_ids},