diff --git a/assets/css/components.css b/assets/css/components.css index bff768bf5..0fc70707d 100644 --- a/assets/css/components.css +++ b/assets/css/components.css @@ -213,16 +213,24 @@ } .menu__content { - @apply absolute z-[100] rounded-lg bg-white flex flex-col py-2 mt-1; + @apply absolute z-[100] rounded-lg bg-white flex flex-col py-2; box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15); } - .menu__content--right { - @apply right-0; + .menu__content--top-right { + @apply top-0 right-0 transform -translate-y-full -mt-1; } - .menu__content--left { - @apply left-0; + .menu__content--top-left { + @apply top-0 left-0 transform -translate-y-full -mt-1; + } + + .menu__content--bottom-right { + @apply bottom-0 right-0 transform translate-y-full -mb-1; + } + + .menu__content--bottom-left { + @apply bottom-0 left-0 transform translate-y-full -mb-1; } .menu:not(.menu--open) > .menu__overlay, diff --git a/assets/css/js_interop.css b/assets/css/js_interop.css index 0e317c2ed..b57443c75 100644 --- a/assets/css/js_interop.css +++ b/assets/css/js_interop.css @@ -259,3 +259,28 @@ solely client-side operations. [phx-hook="VirtualizedLines"]:not(:hover) [data-el-clipcopy] { @apply hidden; } + +[data-el-session][data-js-code-zen] [data-el-section-headline], +[data-el-session][data-js-code-zen] [data-el-section-subheadline], +[data-el-session][data-js-code-zen] [data-el-cell][data-type="markdown"], +[data-el-session][data-js-code-zen] [data-el-actions], +[data-el-session][data-js-code-zen] [data-el-insert-buttons] { + @apply hidden; +} + +[data-el-session][data-js-code-zen] [data-el-sections-container] { + @apply space-y-0 mt-0; +} + +[data-el-session][data-js-code-zen][data-js-no-outputs] + [data-el-outputs-container] { + @apply hidden; +} + +[data-el-session][data-js-code-zen] [data-el-code-zen-enable] { + @apply hidden; +} + +[data-el-session]:not([data-js-code-zen]) [data-el-focus-mode-options] { + @apply hidden; +} diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index 1dda95b72..1e1607fe6 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -1,5 +1,5 @@ import { getAttributeOrThrow, parseInteger } from "../lib/attribute"; -import { randomId, randomToken } from "../lib/utils"; +import { isElementHidden, randomId, randomToken } from "../lib/utils"; import { getChannel, transportDecode, @@ -227,7 +227,7 @@ const JSView = { const { iframe, iframePlaceholder } = this; const notebookEl = document.querySelector(`[data-el-notebook]`); - if (iframePlaceholder.offsetParent === null) { + if (isElementHidden(iframePlaceholder)) { // When the placeholder is hidden, we hide the iframe as well iframe.classList.add("hidden"); } else { diff --git a/assets/js/hooks/session.js b/assets/js/hooks/session.js index 86b469bf0..8df7d745b 100644 --- a/assets/js/hooks/session.js +++ b/assets/js/hooks/session.js @@ -7,6 +7,7 @@ import { setFavicon, cancelEvent, isElementInViewport, + isElementHidden, } from "../lib/utils"; import { getAttributeOrDefault } from "../lib/attribute"; import KeyBuffer from "../lib/key_buffer"; @@ -68,6 +69,7 @@ const Session = { this.focusedId = null; this.insertMode = false; + this.codeZen = false; this.keyBuffer = new KeyBuffer(); this.clientsMap = {}; this.lastLocationReportByClientPid = {}; @@ -121,6 +123,21 @@ const Session = { this.handleCellIndicatorsClick(event) ); + this.getElement("code-zen-enable-button").addEventListener( + "click", + (event) => this.setCodeZen(true) + ); + + this.getElement("code-zen-disable-button").addEventListener( + "click", + (event) => this.setCodeZen(false) + ); + + this.getElement("code-zen-outputs-toggle").addEventListener( + "click", + (event) => this.el.toggleAttribute("data-js-no-outputs") + ); + window.addEventListener( "phx:page-loading-stop", () => { @@ -359,9 +376,11 @@ const Session = { } else if (keyBuffer.tryMatch(["N"])) { this.insertCellAboveFocused("code"); } else if (keyBuffer.tryMatch(["m"])) { - this.insertCellBelowFocused("markdown"); + !this.codeZen && this.insertCellBelowFocused("markdown"); } else if (keyBuffer.tryMatch(["M"])) { - this.insertCellAboveFocused("markdown"); + !this.codeZen && this.insertCellAboveFocused("markdown"); + } else if (keyBuffer.tryMatch(["z"])) { + this.setCodeZen(!this.codeZen); } } }, @@ -852,6 +871,28 @@ const Session = { }); }, + setCodeZen(enabled) { + this.codeZen = enabled; + + if (enabled) { + this.el.setAttribute("data-js-code-zen", ""); + } else { + this.el.removeAttribute("data-js-code-zen"); + } + + if (this.focusedId) { + const visibleId = this.ensureVisibleFocusableEl(this.focusedId); + + if (visibleId !== this.focused) { + this.setFocusedEl(visibleId, { scroll: false }); + } + + if (visibleId) { + this.getFocusableEl(visibleId).scrollIntoView({ block: "center" }); + } + } + }, + // Server event handlers handleCellInserted(cellId) { @@ -863,7 +904,12 @@ const Session = { handleCellDeleted(cellId, siblingCellId) { if (this.focusedId === cellId) { - this.setFocusedEl(siblingCellId); + if (this.codeZen) { + const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId); + this.setFocusedEl(visibleSiblingId); + } else { + this.setFocusedEl(siblingCellId); + } } }, @@ -1061,6 +1107,20 @@ const Session = { } }, + ensureVisibleFocusableEl(cellId) { + const focusableEl = this.getFocusableEl(cellId); + const allFocusableEls = Array.from( + this.el.querySelectorAll(`[data-focusable-id]`) + ); + const idx = allFocusableEls.indexOf(focusableEl); + const visibleSibling = [ + ...allFocusableEls.slice(idx, -1), + ...allFocusableEls.slice(0, idx).reverse(), + ].find((el) => !isElementHidden(el)); + + return visibleSibling && visibleSibling.getAttribute("data-focusable-id"); + }, + isCell(focusableId) { const el = this.getFocusableEl(focusableId); return el.hasAttribute("data-el-cell"); @@ -1081,7 +1141,9 @@ const Session = { }, getFocusableEls() { - return Array.from(this.el.querySelectorAll(`[data-focusable-id]`)); + return Array.from(this.el.querySelectorAll(`[data-focusable-id]`)).filter( + (el) => !isElementHidden(el) + ); }, getFocusableIds() { diff --git a/assets/js/lib/utils.js b/assets/js/lib/utils.js index b08f8d947..2e54fb7e3 100644 --- a/assets/js/lib/utils.js +++ b/assets/js/lib/utils.js @@ -17,6 +17,10 @@ export function isElementInViewport(element) { return box.bottom >= 0 && box.top <= window.innerHeight; } +export function isElementHidden(element) { + return element.offsetParent === null; +} + export function clamp(n, x, y) { return Math.min(Math.max(n, x), y); } diff --git a/lib/livebook_web/live/file_select_component.ex b/lib/livebook_web/live/file_select_component.ex index 1a8677d66..653629cbc 100644 --- a/lib/livebook_web/live/file_select_component.ex +++ b/lib/livebook_web/live/file_select_component.ex @@ -193,7 +193,7 @@ defmodule LivebookWeb.FileSelectComponent do defp file_system_menu_button(assigns) do ~H""" - <.menu id="file-system-menu" disabled={@file_system_select_disabled} position="left"> + <.menu id="file-system-menu" disabled={@file_system_select_disabled} position="bottom-left"> <:toggle> + +
+ <.menu id="focus-mode-menu" position="top-right"> + <:toggle> + + + <:content> + + + + +
+ """ + end + defp global_status(%{status: :evaluating} = assigns) do ~H""" diff --git a/lib/livebook_web/live/session_live/insert_buttons_component.ex b/lib/livebook_web/live/session_live/insert_buttons_component.ex index a503a7c58..bbea5767a 100644 --- a/lib/livebook_web/live/session_live/insert_buttons_component.ex +++ b/lib/livebook_web/live/session_live/insert_buttons_component.ex @@ -14,7 +14,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do phx-value-section_id={@section_id} phx-value-cell_id={@cell_id} >+ Code - <.menu id={"#{@id}-block-menu"} position="left"> + <.menu id={"#{@id}-block-menu"} position="bottom-left"> <:toggle> @@ -61,7 +61,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do <% true -> %> - <.menu id={"#{@id}-smart-menu"} position="left"> + <.menu id={"#{@id}-smart-menu"} position="bottom-left"> <:toggle> diff --git a/lib/livebook_web/live/session_live/section_component.ex b/lib/livebook_web/live/session_live/section_component.ex index 7787ff14a..a68846468 100644 --- a/lib/livebook_web/live/session_live/section_component.ex +++ b/lib/livebook_web/live/session_live/section_component.ex @@ -89,7 +89,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do <%= if @section_view.parent do %> -

+

<.remix_icon icon="git-branch-line" class="text-lg font-normal flip-horizontally leading-none" /> diff --git a/lib/livebook_web/live/session_live/shortcuts_component.ex b/lib/livebook_web/live/session_live/shortcuts_component.ex index 5eb8a6ca1..ad9276a12 100644 --- a/lib/livebook_web/live/session_live/shortcuts_component.ex +++ b/lib/livebook_web/live/session_live/shortcuts_component.ex @@ -91,6 +91,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do %{seq: ["m"], desc: "Insert Markdown cell below", basic: true}, %{seq: ["N"], desc: "Insert Code cell above"}, %{seq: ["M"], desc: "Insert Markdown cell above"}, + %{seq: ["z"], desc: "Toggle code zen"}, %{seq: ["d", "d"], desc: "Delete cell", basic: true}, %{seq: ["e", "e"], desc: "Evaluate cell"}, %{seq: ["e", "s"], desc: "Evaluate section"},