diff --git a/panda.config.ts b/panda.config.ts index d7684f85c3..661ba0bb2f 100644 --- a/panda.config.ts +++ b/panda.config.ts @@ -205,6 +205,12 @@ export default defineConfig({ _test: '0s', }, }, + arrowBobbleAnimation: { + value: { + base: '2000ms', + _test: '0s', + }, + }, }, }, }, diff --git a/src/App.css b/src/App.css index c745fd82b0..10a73757ec 100644 --- a/src/App.css +++ b/src/App.css @@ -2712,3 +2712,12 @@ a.advance-setting-link { .copy-icon-wrapper svg { cursor: pointer; } + +@keyframes bobble { + 0%, 100% { + transform: translateX(-50%) translateY(0); + } + 50% { + transform: translateX(-50%) translateY(10px); + } +} \ No newline at end of file diff --git a/src/components/Content.tsx b/src/components/Content.tsx index 01ee52a5ca..245fa843d5 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames' -import React, { FC, useMemo, useRef, useState } from 'react' +import React, { FC, useCallback, useMemo, useRef, useState } from 'react' +import { useDragDropManager } from 'react-dnd' import { useDispatch, useSelector } from 'react-redux' +import { token } from '../../styled-system/tokens' import Dispatch from '../@types/Dispatch' import SimplePath from '../@types/SimplePath' import { Thunk } from '../@types/Thunk' @@ -10,16 +12,22 @@ import { toggleColorPickerActionCreator as toggleColorPicker } from '../actions/ import { isTouch } from '../browser' import { ABSOLUTE_PATH, HOME_PATH, TUTORIAL2_STEP_SUCCESS } from '../constants' import * as selection from '../device/selection' +import attributeEquals from '../selectors/attributeEquals' import { childrenFilterPredicate, filterAllChildren } from '../selectors/getChildren' import getSetting from '../selectors/getSetting' +import getSortedRank from '../selectors/getSortedRank' +import getThoughtById from '../selectors/getThoughtById' import isTutorial from '../selectors/isTutorial' +import store from '../stores/app' +import viewportStore from '../stores/viewport' import fastClick from '../util/fastClick' import head from '../util/head' import isAbsolute from '../util/isAbsolute' +import parentOf from '../util/parentOf' import publishMode from '../util/publishMode' import Editable from './Editable' import EmptyThoughtspace from './EmptyThoughtspace' -import LayoutTree from './LayoutTree' +import LayoutTree, { TreeThoughtPositioned } from './LayoutTree' import Search from './Search' const transientChildPath = ['TRANSIENT_THOUGHT_ID'] as SimplePath @@ -47,6 +55,66 @@ const Content: FC = () => { return children.length }) const isAbsoluteContext = useSelector(state => isAbsolute(state.rootContext)) + const visibilityRef = useRef<'above' | 'below' | null>(null) + const hoveringPath = useSelector(state => state.hoveringPath) + const contextParentPath = parentOf(hoveringPath || []) + + const isSortedContext = useSelector(state => { + return attributeEquals(state, head(contextParentPath), '=sort', 'Alphabetical') + }) + + const dragDropManager = useDragDropManager() + const monitor = dragDropManager.getMonitor() + + const item = monitor.getItem() + + const sourceThought = useSelector(state => { + const sourceThoughtId = head(item?.path || []) + const sourceThought = getThoughtById(state, sourceThoughtId) + return sourceThought + }) + + const newRank = useSelector(state => getSortedRank(state, head(contextParentPath), sourceThought?.value || '')) + const scrollY = window.scrollY + const viewportHeight = viewportStore.useSelector(state => state.innerHeight) + // The arbitrary space above the first thought + const spaceAbove = 100 + + /** Calculate the visibiltiy of thought being dragged in a sorted context. */ + const calculateHoverArrowPosition = useCallback( + (thoughts: TreeThoughtPositioned[]) => { + const state = store.getState() + visibilityRef.current = null + + let lastSortedThought: TreeThoughtPositioned | null = null + + thoughts.some((thought, index) => { + const path = thought.path + const currentThought = getThoughtById(state, head(path)) + lastSortedThought = isSortedContext ? thought : null + + if (isSortedContext && Math.floor(newRank) === currentThought.rank) { + const y = thought.y + + if (y > viewportHeight + scrollY) { + visibilityRef.current = 'below' + } else if (y < scrollY - spaceAbove) { + visibilityRef.current = 'above' + } + + return true + } + + if (lastSortedThought && lastSortedThought.y > viewportHeight + scrollY) { + visibilityRef.current = 'below' + return true + } + + return false + }) + }, + [isSortedContext, newRank, scrollY, viewportHeight], + ) /** Removes the cursor if the click goes all the way through to the content. Extends cursorBack with logic for closing modals. */ const clickOnEmptySpace: Thunk = (dispatch: Dispatch, getState) => { @@ -91,6 +159,22 @@ const Content: FC = () => { {...fastClick(() => dispatch(clickOnEmptySpace))} onMouseDown={() => setIsPressed(true)} > + {isSortedContext && visibilityRef.current && ( +
+ )} {search != null ? ( ) : ( @@ -101,7 +185,7 @@ const Content: FC = () => { TransientEditable ) : ( /* */ - + )} )} diff --git a/src/components/LayoutTree.tsx b/src/components/LayoutTree.tsx index f295a3888c..3e32e92369 100644 --- a/src/components/LayoutTree.tsx +++ b/src/components/LayoutTree.tsx @@ -68,7 +68,7 @@ type TreeThought = { } /** 2nd Pass: A thought with position information after its height has been measured. */ -type TreeThoughtPositioned = TreeThought & { +export type TreeThoughtPositioned = TreeThought & { cliff: number height: number singleLineHeightWithCliff: number @@ -347,7 +347,7 @@ const linearizeTree = ( } /** Lays out thoughts as DOM siblings with manual x,y positioning. */ -const LayoutTree = () => { +const LayoutTree = ({ calculateThoughtPosition }: { calculateThoughtPosition: any }) => { const { sizes, setSize } = useSizeTracking() const treeThoughts = useSelector(linearizeTree, _.isEqual) const fontSize = useSelector(state => state.fontSize) @@ -591,6 +591,11 @@ const LayoutTree = () => { // get the scroll position before the render so it can be preserved const scrollY = window.scrollY + // When a thought is hovered over a sorted context, determine the position of arrow. + useEffect(() => { + calculateThoughtPosition(treeThoughtsPositioned) + }, [calculateThoughtPosition, treeThoughtsPositioned]) + // when spaceAbove changes, scroll by the same amount so that the thoughts appear to stay in the same place useEffect( () => {