Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/children.tsx
5964 views
import React, { useCallback, useRef } from "react";1import { Virtuoso, VirtuosoHandle } from "react-virtuoso";2import { Ancestor, Descendant, Editor, Element, Range } from "slate";34import { shallowCompare } from "@cocalc/util/misc";56import { SlateEditor } from "../../editable-markdown";7import { ReactEditor } from "..";8import { useDecorate } from "../hooks/use-decorate";9import { useSlateStatic } from "../hooks/use-slate-static";10import { NODE_TO_INDEX, NODE_TO_PARENT } from "../utils/weak-maps";11import ElementComponent from "./element";12import { RenderElementProps, RenderLeafProps } from "./editable";13import TextComponent from "./text";1415export interface WindowingParams {16rowStyle?: React.CSSProperties;17overscanRowCount?: number;18estimatedRowSize?: number;19marginTop?;20marginBottom?;21rowSizeEstimator?: (Node) => number | undefined;22}2324/**25* Children.26*/2728interface Props {29decorations: Range[];30node: Ancestor;31renderElement?: React.FC<RenderElementProps>;32renderLeaf?: React.FC<RenderLeafProps>;33selection: Range | null;34windowing?: WindowingParams;35onScroll?: () => void; // called after scrolling when windowing is true.36isComposing?: boolean;37hiddenChildren?: Set<number>;38}3940const Children: React.FC<Props> = React.memo(41({42decorations,43node,44renderElement,45renderLeaf,46selection,47windowing,48onScroll,49hiddenChildren,50}) => {51const decorate = useDecorate();52const editor = useSlateStatic() as SlateEditor;53const virtuosoRef = useRef<VirtuosoHandle>(null);54const scrollerRef = useRef<HTMLDivElement | null>(null);55// see https://github.com/petyosi/react-virtuoso/issues/27456const handleScrollerRef = useCallback((ref) => {57scrollerRef.current = ref;58}, []);59let path;60try {61path = ReactEditor.findPath(editor, node);62} catch (err) {63console.warn("WARNING: unable to find path to node", node, err);64return <></>;65}66//console.log("render Children", path);6768const isLeafBlock =69Element.isElement(node) &&70!editor.isInline(node) &&71Editor.hasInlines(editor, node);7273const renderChild = ({ index }: { index: number }) => {74//console.log("renderChild", index, JSON.stringify(selection));75// When windowing, we put a margin at the top of the first cell76// and the bottom of the last cell. This makes sure the scroll77// bar looks right, which it would not if we put a margin around78// the entire list.79let marginTop: string | undefined = undefined;80let marginBottom: string | undefined = undefined;81if (windowing != null) {82if (windowing.marginTop && index === 0) {83marginTop = windowing.marginTop;84} else if (85windowing.marginBottom &&86index + 1 === node?.children?.length87) {88marginBottom = windowing.marginBottom;89}90}9192if (hiddenChildren?.has(index)) {93// TRICK: We use a small positive height since a height of 0 gets ignored, as it often94// appears when scrolling and allowing that breaks everything (for now!).95return (96<div97style={{ height: "1px", marginTop, marginBottom }}98contentEditable={false}99/>100);101}102const n = node.children[index] as Descendant;103const key = ReactEditor.findKey(editor, n);104let ds, range;105if (path != null) {106const p = path.concat(index);107try {108// I had the following crash once when pasting, then undoing in production:109range = Editor.range(editor, p);110} catch (_) {111range = null;112}113if (range != null) {114ds = decorate([n, p]);115for (const dec of decorations) {116const d = Range.intersection(dec, range);117118if (d) {119ds.push(d);120}121}122}123} else {124ds = [];125range = null;126}127128if (Element.isElement(n)) {129const x = (130<ElementComponent131decorations={ds}132element={n}133key={key.id}134renderElement={renderElement}135renderLeaf={renderLeaf}136selection={137selection && range && Range.intersection(range, selection)138}139/>140);141if (marginTop || marginBottom) {142return <div style={{ marginTop, marginBottom }}>{x}</div>;143} else {144return x;145}146} else {147return (148<TextComponent149decorations={ds ?? []}150key={key.id}151isLast={isLeafBlock && index === node.children.length - 1}152parent={node as Element}153renderLeaf={renderLeaf}154text={n}155/>156);157}158};159160for (let i = 0; i < node.children.length; i++) {161const n = node.children[i];162NODE_TO_INDEX.set(n, i);163NODE_TO_PARENT.set(n, node);164}165166if (windowing != null) {167// using windowing168169// This is slightly awkward since when splitting frames, the component170// gets unmounted and then mounted again, in which case editor.windowedListRef.current171// does not get set to null, so we need to write the new virtuosoRef;172if (editor.windowedListRef.current == null) {173editor.windowedListRef.current = {};174}175editor.windowedListRef.current.virtuosoRef = virtuosoRef;176editor.windowedListRef.current.getScrollerRef = () => scrollerRef.current; // we do this so windowListRef is JSON-able!177178// NOTE: the code for preserving scroll position when editing assumes179// the visibleRange really is *visible*. Thus if you mess with overscan180// or related properties below, that will likely break.181return (182<Virtuoso183ref={virtuosoRef}184scrollerRef={handleScrollerRef}185onScroll={onScroll}186className="smc-vfill"187totalCount={node.children.length}188itemContent={(index) => (189<div style={windowing.rowStyle}>{renderChild({ index })}</div>190)}191computeItemKey={(index) =>192ReactEditor.findKey(editor, node.children[index])?.id ?? `${index}`193}194rangeChanged={(visibleRange) => {195editor.windowedListRef.current.visibleRange = visibleRange;196}}197itemsRendered={(items) => {198const scrollTop = scrollerRef.current?.scrollTop ?? 0;199// need both items, since may use first if there is no second...200editor.windowedListRef.current.firstItemOffset =201scrollTop - items[0]?.offset;202editor.windowedListRef.current.secondItemOffset =203scrollTop - items[1]?.offset;204}}205/>206);207} else {208// anything else -- just render the children209const children: React.JSX.Element[] = [];210for (let index = 0; index < node.children.length; index++) {211try {212children.push(renderChild({ index }));213} catch (err) {214console.warn(215"SLATE -- issue in renderChild",216node.children[index],217err,218);219}220}221222return <>{children}</>;223}224},225(prev, next) => {226if (next.isComposing) {227// IMPORTANT: We prevent render while composing, since rendering228// would corrupt the DOM which confuses composition input, thus229// breaking input on Android, and many non-US languages. See230// https://github.com/ianstormtaylor/slate/issues/4127#issuecomment-803215432231return true;232}233return shallowCompare(prev, next);234},235);236237export default Children;238239/*240function getCursorY(): number | null {241const sel = getSelection();242if (sel == null || sel.rangeCount == 0) {243return null;244}245return sel.getRangeAt(0)?.getBoundingClientRect().y;246}247248function preserveCursorScrollPosition() {249const before = getCursorY();250if (before === null) return;251requestAnimationFrame(() => {252const after = getCursorY();253if (after === null) return;254const elt = $('[data-virtuoso-scroller="true"]');255if (elt) {256elt.scrollTop((elt.scrollTop() ?? 0) + (after - before));257}258});259}260*/261262263