mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-11-18 05:44:12 +08:00
6c53c09a61
* Add copy to clipboard button to virtualized output * Move text output into its own component * Update button background
144 lines
4 KiB
JavaScript
144 lines
4 KiB
JavaScript
import HyperList from "hyperlist";
|
|
import {
|
|
getAttributeOrThrow,
|
|
parseBoolean,
|
|
parseInteger,
|
|
} from "../lib/attribute";
|
|
import { getLineHeight } from "../lib/utils";
|
|
|
|
/**
|
|
* A hook used to render text lines as a virtual list,
|
|
* so that only the visible lines are actually in the DOM.
|
|
*
|
|
* Configuration:
|
|
*
|
|
* * `data-max-height` - the maximum height of the element, exceeding this height enables scrolling
|
|
*
|
|
* * `data-follow` - whether to automatically scroll to the bottom as new lines appear
|
|
*
|
|
* The element should have two children:
|
|
*
|
|
* * one annotated with `data-template` attribute, it should be hidden
|
|
* and contain all the line elements as its children
|
|
*
|
|
* * 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() {
|
|
this.props = getProps(this);
|
|
this.state = {
|
|
lineHeight: null,
|
|
templateElement: null,
|
|
contentElement: null,
|
|
virtualizedList: null,
|
|
};
|
|
|
|
this.state.lineHeight = getLineHeight(this.el);
|
|
|
|
this.state.templateElement = this.el.querySelector("[data-template]");
|
|
|
|
if (!this.state.templateElement) {
|
|
throw new Error(
|
|
"VirtualizedLines must have a child with data-template attribute"
|
|
);
|
|
}
|
|
|
|
this.state.contentElement = this.el.querySelector("[data-content]");
|
|
|
|
if (!this.state.contentElement) {
|
|
throw new Error(
|
|
"VirtualizedLines must have a child with data-content attribute"
|
|
);
|
|
}
|
|
|
|
const config = hyperListConfig(
|
|
this.state.contentElement,
|
|
this.state.templateElement,
|
|
this.props.maxHeight,
|
|
this.state.lineHeight
|
|
);
|
|
this.state.virtualizedList = new HyperList(
|
|
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() {
|
|
this.props = getProps(this);
|
|
|
|
const config = hyperListConfig(
|
|
this.state.contentElement,
|
|
this.state.templateElement,
|
|
this.props.maxHeight,
|
|
this.state.lineHeight
|
|
);
|
|
|
|
const scrollTop = Math.round(this.state.contentElement.scrollTop);
|
|
const maxScrollTop = Math.round(
|
|
this.state.contentElement.scrollHeight -
|
|
this.state.contentElement.clientHeight
|
|
);
|
|
const isAtTheEnd = scrollTop === maxScrollTop;
|
|
|
|
this.state.virtualizedList.refresh(this.state.contentElement, config);
|
|
|
|
if (this.props.follow && isAtTheEnd) {
|
|
this.state.contentElement.scrollTop = this.state.contentElement.scrollHeight;
|
|
}
|
|
},
|
|
};
|
|
|
|
function hyperListConfig(
|
|
contentElement,
|
|
templateElement,
|
|
maxHeight,
|
|
lineHeight
|
|
) {
|
|
const numberOfLines = templateElement.childElementCount;
|
|
const height = Math.min(maxHeight, lineHeight * numberOfLines);
|
|
|
|
return {
|
|
height,
|
|
total: numberOfLines,
|
|
itemHeight: lineHeight,
|
|
generate: (index) => {
|
|
// Clone n-th child of the template container.
|
|
return templateElement.children.item(index).cloneNode(true);
|
|
},
|
|
afterRender: () => {
|
|
// The content element has a fixed height and when the horizontal
|
|
// scrollbar appears, it's treated as part of the element's content.
|
|
// To accommodate for the scrollbar we dynamically add more height
|
|
// to the element.
|
|
if (contentElement.scrollWidth > contentElement.clientWidth) {
|
|
contentElement.style.height = `${height + 12}px`;
|
|
} else {
|
|
contentElement.style.height = `${height}px`;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function getProps(hook) {
|
|
return {
|
|
maxHeight: getAttributeOrThrow(hook.el, "data-max-height", parseInteger),
|
|
follow: getAttributeOrThrow(hook.el, "data-follow", parseBoolean),
|
|
};
|
|
}
|
|
|
|
export default VirtualizedLines;
|