Path: blob/master/src/packages/frontend/editors/slate/slate-react/components/children.tsx
1698 views
import React, { useCallback, useRef } from "react";1import { Editor, Range, Element, Ancestor, Descendant } from "slate";23import ElementComponent from "./element";4import TextComponent from "./text";5import { ReactEditor } from "..";6import { useSlateStatic } from "../hooks/use-slate-static";7import { useDecorate } from "../hooks/use-decorate";8import { NODE_TO_INDEX, NODE_TO_PARENT } from "../utils/weak-maps";9import { RenderElementProps, RenderLeafProps } from "./editable";10import { Virtuoso, VirtuosoHandle } from "react-virtuoso";11import { shallowCompare } from "@cocalc/util/misc";12import { SlateEditor } from "../../editable-markdown";1314export interface WindowingParams {15rowStyle?: React.CSSProperties;16overscanRowCount?: number;17estimatedRowSize?: number;18marginTop?;19marginBottom?;20rowSizeEstimator?: (Node) => number | undefined;21}2223/**24* Children.25*/2627interface Props {28decorations: Range[];29node: Ancestor;30renderElement?: React.FC<RenderElementProps>;31renderLeaf?: React.FC<RenderLeafProps>;32selection: Range | null;33windowing?: WindowingParams;34onScroll?: () => void; // called after scrolling when windowing is true.35isComposing?: boolean;36hiddenChildren?: Set<number>;37}3839const Children: React.FC<Props> = React.memo(40({41decorations,42node,43renderElement,44renderLeaf,45selection,46windowing,47onScroll,48hiddenChildren,49}) => {50const decorate = useDecorate();51const editor = useSlateStatic() as SlateEditor;52let path;53try {54path = ReactEditor.findPath(editor, node);55} catch (err) {56console.warn("WARNING: unable to find path to node", node, err);57return <></>;58}59//console.log("render Children", path);6061const isLeafBlock =62Element.isElement(node) &&63!editor.isInline(node) &&64Editor.hasInlines(editor, node);6566const renderChild = ({ index }: { index: number }) => {67//console.log("renderChild", index, JSON.stringify(selection));68// When windowing, we put a margin at the top of the first cell69// and the bottom of the last cell. This makes sure the scroll70// bar looks right, which it would not if we put a margin around71// the entire list.72let marginTop: string | undefined = undefined;73let marginBottom: string | undefined = undefined;74if (windowing != null) {75if (windowing.marginTop && index === 0) {76marginTop = windowing.marginTop;77} else if (78windowing.marginBottom &&79index + 1 === node?.children?.length80) {81marginBottom = windowing.marginBottom;82}83}8485if (hiddenChildren?.has(index)) {86// TRICK: We use a small positive height since a height of 0 gets ignored, as it often87// appears when scrolling and allowing that breaks everything (for now!).88return (89<div90style={{ height: "1px", marginTop, marginBottom }}91contentEditable={false}92/>93);94}95const n = node.children[index] as Descendant;96const key = ReactEditor.findKey(editor, n);97let ds, range;98if (path != null) {99const p = path.concat(index);100try {101// I had the following crash once when pasting, then undoing in production:102range = Editor.range(editor, p);103} catch (_) {104range = null;105}106if (range != null) {107ds = decorate([n, p]);108for (const dec of decorations) {109const d = Range.intersection(dec, range);110111if (d) {112ds.push(d);113}114}115}116} else {117ds = [];118range = null;119}120121if (Element.isElement(n)) {122const x = (123<ElementComponent124decorations={ds}125element={n}126key={key.id}127renderElement={renderElement}128renderLeaf={renderLeaf}129selection={130selection && range && Range.intersection(range, selection)131}132/>133);134if (marginTop || marginBottom) {135return <div style={{ marginTop, marginBottom }}>{x}</div>;136} else {137return x;138}139} else {140return (141<TextComponent142decorations={ds ?? []}143key={key.id}144isLast={isLeafBlock && index === node.children.length - 1}145parent={node as Element}146renderLeaf={renderLeaf}147text={n}148/>149);150}151};152153for (let i = 0; i < node.children.length; i++) {154const n = node.children[i];155NODE_TO_INDEX.set(n, i);156NODE_TO_PARENT.set(n, node);157}158159const virtuosoRef = useRef<VirtuosoHandle>(null);160const scrollerRef = useRef<HTMLDivElement | null>(null);161// see https://github.com/petyosi/react-virtuoso/issues/274162const handleScrollerRef = useCallback((ref) => {163scrollerRef.current = ref;164}, []);165if (windowing != null) {166// using windowing167168// This is slightly awkward since when splitting frames, the component169// gets unmounted and then mounted again, in which case editor.windowedListRef.current170// does not get set to null, so we need to write the new virtuosoRef;171if (editor.windowedListRef.current == null) {172editor.windowedListRef.current = {};173}174editor.windowedListRef.current.virtuosoRef = virtuosoRef;175editor.windowedListRef.current.getScrollerRef = () => scrollerRef.current; // we do this so windowListRef is JSON-able!176177// NOTE: the code for preserving scroll position when editing assumes178// the visibleRange really is *visible*. Thus if you mess with overscan179// or related properties below, that will likely break.180return (181<Virtuoso182ref={virtuosoRef}183scrollerRef={handleScrollerRef}184onScroll={onScroll}185className="smc-vfill"186totalCount={node.children.length}187itemContent={(index) => (188<div style={windowing.rowStyle}>{renderChild({ index })}</div>189)}190computeItemKey={(index) =>191ReactEditor.findKey(editor, node.children[index])?.id ?? `${index}`192}193rangeChanged={(visibleRange) => {194editor.windowedListRef.current.visibleRange = visibleRange;195}}196itemsRendered={(items) => {197const scrollTop = scrollerRef.current?.scrollTop ?? 0;198// need both items, since may use first if there is no second...199editor.windowedListRef.current.firstItemOffset =200scrollTop - items[0]?.offset;201editor.windowedListRef.current.secondItemOffset =202scrollTop - items[1]?.offset;203}}204/>205);206} else {207// anything else -- just render the children208const children: React.JSX.Element[] = [];209for (let index = 0; index < node.children.length; index++) {210try {211children.push(renderChild({ index }));212} catch (err) {213console.warn(214"SLATE -- issue in renderChild",215node.children[index],216err217);218}219}220221return <>{children}</>;222}223},224(prev, next) => {225if (next.isComposing) {226// IMPORTANT: We prevent render while composing, since rendering227// would corrupt the DOM which confuses composition input, thus228// breaking input on Android, and many non-US languages. See229// https://github.com/ianstormtaylor/slate/issues/4127#issuecomment-803215432230return true;231}232return shallowCompare(prev, next);233}234);235236export default Children;237238/*239function getCursorY(): number | null {240const sel = getSelection();241if (sel == null || sel.rangeCount == 0) {242return null;243}244return sel.getRangeAt(0)?.getBoundingClientRect().y;245}246247function preserveCursorScrollPosition() {248const before = getCursorY();249if (before === null) return;250requestAnimationFrame(() => {251const after = getCursorY();252if (after === null) return;253const elt = $('[data-virtuoso-scroller="true"]');254if (elt) {255elt.scrollTop((elt.scrollTop() ?? 0) + (after - before));256}257});258}259*/260261262