From 2e76f415f9561ef24b5cb91058486a18b56672f7 Mon Sep 17 00:00:00 2001 From: Raine Revere Date: Fri, 27 Dec 2024 19:30:01 +0000 Subject: [PATCH] Move ContextBreadcrumbs from Thought to LayoutTree. --- src/components/LayoutTree.tsx | 128 ++++++++++++++++++++++++++++------ src/components/Thought.tsx | 35 ---------- 2 files changed, 106 insertions(+), 57 deletions(-) diff --git a/src/components/LayoutTree.tsx b/src/components/LayoutTree.tsx index 8a986c0d46..c3e3d1dfe1 100644 --- a/src/components/LayoutTree.tsx +++ b/src/components/LayoutTree.tsx @@ -41,6 +41,7 @@ import parentOf from '../util/parentOf' import parseLet from '../util/parseLet' import safeRefMerge from '../util/safeRefMerge' import unroot from '../util/unroot' +import ContextBreadcrumbs from './ContextBreadcrumbs' import DropEnd from './DropEnd' import FadeTransition from './FadeTransition' import HoverArrow from './HoverArrow' @@ -93,9 +94,11 @@ type TreeThoughtPositioned = TreeThought & { /** 2nd Pass: A context breadcrumb. */ type TreeContextBreadcrumbs = { type: 'context' - path: Path + key: string x: number y: number + path: Path + simplePath: SimplePath } /** Returns true if the positioned node is a thought node. */ @@ -390,8 +393,70 @@ const linearizeTree = ( } /** A positioned context breadcrumbs component. */ -const TreeContextNode = ({ x, y, path }: TreeContextBreadcrumbs) => { - return null +const TreeContextNode = ({ + x, + y: _y, + path, + simplePath, + thoughtKey, + ...transitionGroupsProps +}: TreeContextBreadcrumbs & { + // cannot pass React key as prop, so use a new prop + thoughtKey: string +} & Pick) => { + const [y, setY] = useState(_y) + const fadeThoughtRef = useRef(null) + + useLayoutEffect(() => { + if (y !== _y) { + // When y changes React re-renders the component with the new value of y. It will result in a visual change in the DOM. + // Because this is a state-driven change, React applies the updated value to the DOM, which causes the browser to recognize that + // a CSS property has changed, thereby triggering the CSS transition. + // Without this additional render, updates get batched and subsequent CSS transitions may not work properly. For example, when moving a thought down, it would not animate. + setY(_y) + } + }, [y, _y]) + + const homeContext = useSelector(state => { + const pathParent = rootedParentOf(state, path) + const showContexts = isContextViewActive(state, path) + return showContexts && isRoot(pathParent) + }) + + // List Virtualization + // Do not render thoughts that are below the viewport. + // Exception: The cursor thought and its previous siblings may temporarily be out of the viewport, such as if when New Subthought is activated on a long context. In this case, the new thought will be created below the viewport and needs to be rendered in order for scrollCursorIntoView to be activated. + // Render virtualized thoughts with their estimated height so that document height is relatively stable. + // Perform this check here instead of in virtualThoughtsPositioned since it changes with the scroll position (though currently `sizes` will change as new thoughts are rendered, causing virtualThoughtsPositioned to re-render anyway). + // if (belowCursor && !isCursor && y > viewportBottom + height) return null + + return ( +
+ +
+ +
+
+
+ ) } /** Renders a thought component for mapped treeNodesPositioned. */ @@ -775,7 +840,7 @@ const LayoutTree = () => { // cache table column 1 widths so they are only calculated once and then assigned to each thought in the column // key thoughtId of thought with =table attribute const tableCol1Widths = new Map() - const treeNodesPositioned = treeThoughts.map((node, i) => { + const treeNodesPositioned = treeThoughts.flatMap((node, i) => { const next: TreeThought | undefined = treeThoughts[i + 1] // cliff is the number of levels that drop off after the last thought at a given depth. Increase in depth is ignored. @@ -844,10 +909,11 @@ const LayoutTree = () => { // capture the y position of the current thought before it is incremented by its own height const y = yaccum + const contextBreadcrumbsHeight = singleLineHeight * 0.7 // increase y by the height of the current thought if (!node.isTableCol1 || node.leaf) { - yaccum += height + yaccum += height + (node.showContexts ? contextBreadcrumbsHeight : 0) } // if the current thought is in table col1, push its y and depth onto the stack so that the next node after it can be positioned below it instead of overlapping it @@ -877,17 +943,31 @@ const LayoutTree = () => { } } - return { - type: 'thought' as 'thought' | 'context', - ...node, - cliff, - height, - singleLineHeightWithCliff, - width: tableCol1Widths.get(head(parentOf(node.path))), - isLastVisible, - x, - y, - } + return [ + ...(node.showContexts + ? [ + { + type: 'context' as 'thought' | 'context', + key: node.key, + x, + y, + path: node.path, + simplePath: node.simplePath, + }, + ] + : []), + { + type: 'thought' as 'thought' | 'context', + ...node, + cliff, + height, + singleLineHeightWithCliff, + width: tableCol1Widths.get(head(parentOf(node.path))), + isLastVisible, + x, + y: y + (node.showContexts ? contextBreadcrumbsHeight : 0), + }, + ] as (TreeThoughtPositioned | TreeContextBreadcrumbs)[] }) // Determine hoverArrowVisibility based on newRank and the visible thoughts @@ -969,15 +1049,15 @@ const LayoutTree = () => { }} > - {treeNodesPositioned.map((thought, index) => - isThoughtNode(thought) ? ( + {treeNodesPositioned.map((node, index) => + isThoughtNode(node) ? ( { }} /> ) : ( - + ), )} diff --git a/src/components/Thought.tsx b/src/components/Thought.tsx index f1542577d9..4d5a7fbda1 100644 --- a/src/components/Thought.tsx +++ b/src/components/Thought.tsx @@ -10,7 +10,6 @@ import Path from '../@types/Path' import SimplePath from '../@types/SimplePath' import Thought from '../@types/Thought' import ThoughtId from '../@types/ThoughtId' -import { expandContextThoughtActionCreator as expandContextThought } from '../actions/expandContextThought' import { toggleMulticursorActionCreator as toggleMulticursor } from '../actions/toggleMulticursor' import { isMac, isTouch } from '../browser' import { AlertType, MAX_DISTANCE_FROM_CURSOR, REGEX_TAGS } from '../constants' @@ -36,19 +35,16 @@ import distractionFreeTypingStore from '../stores/distractionFreeTyping' import containsURL from '../util/containsURL' import equalPath from '../util/equalPath' import equalThoughtRanked from '../util/equalThoughtRanked' -import fastClick from '../util/fastClick' import hashPath from '../util/hashPath' import head from '../util/head' import isAttribute from '../util/isAttribute' import isDescendantPath from '../util/isDescendantPath' import isDivider from '../util/isDivider' -import isRoot from '../util/isRoot' import parentOf from '../util/parentOf' import publishMode from '../util/publishMode' import safeRefMerge from '../util/safeRefMerge' import Bullet from './Bullet' import Byline from './Byline' -import ContextBreadcrumbs from './ContextBreadcrumbs' import DropHover from './DropHover' import Note from './Note' import StaticThought from './StaticThought' @@ -203,12 +199,6 @@ const ThoughtContainer = ({ toggleMulticursorOnLongPress: true, }) - const homeContext = useSelector(state => { - const pathParent = rootedParentOf(state, path) - const showContexts = isContextViewActive(state, path) - return showContexts && isRoot(pathParent) - }) - // true if the thought has an invalid option const invalidOption = useSelector(state => { const thought = getThoughtById(state, thoughtId) @@ -389,31 +379,6 @@ const ThoughtContainer = ({ }), )} > - {showContexts && simplePath.length > 1 ? ( - - ) : showContexts && simplePath.length > 2 ? ( - - { - dispatch(expandContextThought(path)) - })} - > - ...{' '} - - - ) : null} -