diff --git a/assets/js/hooks/virtualized_lines.js b/assets/js/hooks/virtualized_lines.js index 7b88c1f4ddf..b8b0ca33194 100644 --- a/assets/js/hooks/virtualized_lines.js +++ b/assets/js/hooks/virtualized_lines.js @@ -2,6 +2,7 @@ import HyperList from "hyperlist"; import { parseHookProps } from "../lib/attribute"; import { findChildOrThrow, + findClosestOrThrow, getLineHeight, isScrolledToEnd, scrollToEnd, @@ -16,6 +17,10 @@ import { * * `max-height` - the maximum height of the element, exceeding * this height enables scrolling * + * * `max-height-amplified` - the maximum height of the element + * when "Amplify Output" is selected in the UI, exceeding this + * height enables scrolling when selected + * * * `follow` - whether to automatically scroll to the bottom as * new lines appear * @@ -40,6 +45,9 @@ const VirtualizedLines = { this.lineHeight = getLineHeight(this.el); this.templateEl = findChildOrThrow(this.el, "[data-template]"); this.contentEl = findChildOrThrow(this.el, "[data-content]"); + this.cellEl = findClosestOrThrow(this.el, "[data-el-cell]"); + this.amplifyOutput = this.isAmplifyOutput(); + this.amplifyObserver = this.newAmplifyObserver(); this.capLines(); @@ -67,9 +75,14 @@ const VirtualizedLines = { } }, + destroyed() { + this.amplifyObserver.disconnect(); + }, + getProps() { return parseHookProps(this.el, [ "max-height", + "max-height-amplified", "follow", "max-lines", "ignore-trailing-empty-line", @@ -81,7 +94,7 @@ const VirtualizedLines = { const numberOfLines = lineEls.length; const height = Math.min( - this.props.maxHeight, + this.amplifyOutput ? this.props.maxHeightAmplified : this.props.maxHeight, this.lineHeight * numberOfLines, ); @@ -139,6 +152,25 @@ const VirtualizedLines = { } } }, + + newAmplifyObserver() { + const observer = new MutationObserver((mutationRecords) => { + if ( + mutationRecords.length != 1 || + mutationRecords[0].target != this.cellEl || + mutationRecords[0].attributeName != "data-js-amplified" + ) { throw new Error("unexpected mutation changing Amplify Output"); } + + this.amplifyOutput = this.isAmplifyOutput(); + this.updated(); + }); + observer.observe(this.cellEl, {attributeFilter: ["data-js-amplified"]}); + return observer; + }, + + isAmplifyOutput() { + return (this.cellEl.getAttribute("data-js-amplified") != null); + }, }; export default VirtualizedLines; diff --git a/assets/js/lib/utils.js b/assets/js/lib/utils.js index e58c539899e..a196eedadae 100644 --- a/assets/js/lib/utils.js +++ b/assets/js/lib/utils.js @@ -241,6 +241,18 @@ export function findChildOrThrow(element, selector) { return child; } +export function findClosestOrThrow(element, selector) { + const closest = element.closest(selector); + + if (!closest) { + throw new Error( + `expected closest matching ${selector}, but none was found`, + ); + } + + return closest; +} + export function cancelEvent(event) { // Cancel any default browser behavior. event.preventDefault(); diff --git a/lib/livebook_web/live/output/terminal_text_component.ex b/lib/livebook_web/live/output/terminal_text_component.ex index 3467c66403f..1c7eeadeca6 100644 --- a/lib/livebook_web/live/output/terminal_text_component.ex +++ b/lib/livebook_web/live/output/terminal_text_component.ex @@ -64,6 +64,7 @@ defmodule LivebookWeb.Output.TerminalTextComponent do class="relative group/root" phx-hook="VirtualizedLines" data-p-max-height={hook_prop(300)} + data-p-max-height-amplified={hook_prop(600)} data-p-follow={hook_prop(true)} data-p-max-lines={hook_prop(Livebook.Notebook.max_terminal_lines())} data-p-ignore-trailing-empty-line={hook_prop(true)}