mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-02-21 21:36:17 +08:00
164 lines
4.2 KiB
JavaScript
164 lines
4.2 KiB
JavaScript
import HyperList from "hyperlist";
|
|
import {
|
|
getAttributeOrDefault,
|
|
getAttributeOrThrow,
|
|
parseBoolean,
|
|
parseInteger,
|
|
} from "../lib/attribute";
|
|
import {
|
|
findChildOrThrow,
|
|
getLineHeight,
|
|
isScrolledToEnd,
|
|
scrollToEnd,
|
|
} 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. Defaults to false
|
|
*
|
|
* * `data-max-lines` - the maximum number of lines to keep in the DOM.
|
|
* By default all lines are kept
|
|
*
|
|
* * `data-ignore-trailing-empty-line` - whether to ignore the last
|
|
* line if it is empty. Defaults to false
|
|
*
|
|
* The element should have two children:
|
|
*
|
|
* * `[data-template]` - a hidden container containing all the line
|
|
* elements, each with a data-line attribute
|
|
*
|
|
* * `[data-content]` - the target element to render the virtualized
|
|
* lines into, it should contain the styling relevant text styles
|
|
*/
|
|
const VirtualizedLines = {
|
|
mounted() {
|
|
this.props = this.getProps();
|
|
|
|
this.lineHeight = getLineHeight(this.el);
|
|
this.templateEl = findChildOrThrow(this.el, "[data-template]");
|
|
this.contentEl = findChildOrThrow(this.el, "[data-content]");
|
|
|
|
this.capLines();
|
|
|
|
const config = this.hyperListConfig();
|
|
this.virtualizedList = new HyperList(this.contentEl, config);
|
|
|
|
if (this.props.follow) {
|
|
scrollToEnd(this.contentEl);
|
|
}
|
|
},
|
|
|
|
updated() {
|
|
this.props = this.getProps();
|
|
|
|
this.capLines();
|
|
|
|
const shouldScrollToEnd =
|
|
this.props.follow && isScrolledToEnd(this.contentEl);
|
|
|
|
const config = this.hyperListConfig();
|
|
this.virtualizedList.refresh(this.contentEl, config);
|
|
|
|
if (shouldScrollToEnd) {
|
|
scrollToEnd(this.contentEl);
|
|
}
|
|
},
|
|
|
|
getProps() {
|
|
return {
|
|
maxHeight: getAttributeOrThrow(this.el, "data-max-height", parseInteger),
|
|
follow: getAttributeOrDefault(
|
|
this.el,
|
|
"data-follow",
|
|
false,
|
|
parseBoolean
|
|
),
|
|
maxLines: getAttributeOrDefault(
|
|
this.el,
|
|
"data-max-lines",
|
|
null,
|
|
parseInteger
|
|
),
|
|
ignoreTrailingEmptyLine: getAttributeOrDefault(
|
|
this.el,
|
|
"data-ignore-trailing-empty-line",
|
|
false,
|
|
parseBoolean
|
|
),
|
|
};
|
|
},
|
|
|
|
hyperListConfig() {
|
|
const lineEls = this.getLineElements();
|
|
const numberOfLines = lineEls.length;
|
|
|
|
const height = Math.min(
|
|
this.props.maxHeight,
|
|
this.lineHeight * numberOfLines
|
|
);
|
|
|
|
return {
|
|
height,
|
|
total: numberOfLines,
|
|
itemHeight: this.lineHeight,
|
|
generate: (index) => {
|
|
const node = lineEls[index].cloneNode(true);
|
|
node.removeAttribute("id");
|
|
return node;
|
|
},
|
|
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 (this.contentEl.scrollWidth > this.contentEl.clientWidth) {
|
|
this.contentEl.style.height = `${height + 12}px`;
|
|
} else {
|
|
this.contentEl.style.height = `${height}px`;
|
|
}
|
|
},
|
|
};
|
|
},
|
|
|
|
getLineElements() {
|
|
const lineEls = Array.from(this.templateEl.querySelectorAll("[data-line]"));
|
|
|
|
if (lineEls.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const lastLineEl = lineEls[lineEls.length - 1];
|
|
|
|
if (this.props.ignoreTrailingEmptyLine && lastLineEl.innerText === "") {
|
|
return lineEls.slice(0, -1);
|
|
} else {
|
|
return lineEls;
|
|
}
|
|
},
|
|
|
|
capLines() {
|
|
if (this.props.maxLines) {
|
|
const lineEls = Array.from(
|
|
this.templateEl.querySelectorAll("[data-line]")
|
|
);
|
|
const ignoredLineEls = lineEls.slice(0, -this.props.maxLines);
|
|
|
|
const [first, ...rest] = ignoredLineEls;
|
|
rest.forEach((lineEl) => lineEl.remove());
|
|
|
|
if (first) {
|
|
first.innerHTML = "...";
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
export default VirtualizedLines;
|