From cd0e2086de6be2122e317692f65769665ddbb3e0 Mon Sep 17 00:00:00 2001 From: 0xzion <0xyz.penx@gmail.com> Date: Sun, 12 Nov 2023 10:04:10 +0800 Subject: [PATCH] feat: can dnd in List --- apps/web/next.config.mjs | 1 + extensions/list/package.json | 4 + extensions/list/src/hooks/useCollapsed.tsx | 21 ++ extensions/list/src/index.ts | 6 +- .../list/src/{ => plugins}/withEditable.ts | 0 .../list/src/{ => plugins}/withListsPlugin.ts | 2 +- .../list/src/{ => plugins}/withMarkdown.ts | 2 +- extensions/list/src/ui/GuideLine.tsx | 7 + extensions/list/src/ui/List.tsx | 15 +- extensions/list/src/ui/ListItem.tsx | 13 +- extensions/list/src/ui/ListItemContent.tsx | 73 +++++- packages/app/package.json | 1 + .../app/src/EditorLayout/EditorLayout.tsx | 2 +- .../app/src/EditorLayout/SidebarDrawer.tsx | 2 - packages/app/src/NodeContent.tsx | 3 + .../app/src/Sidebar/NodeQuery/NodeItem.tsx | 51 ---- .../src/Sidebar/NodeQuery/NodeItemMenu.tsx | 46 ---- .../app/src/Sidebar/NodeQuery/NodeQuery.tsx | 47 ---- packages/app/src/Sidebar/RecentlyEdited.tsx | 7 - packages/app/src/Sidebar/RecentlyOpened.tsx | 7 - packages/app/src/Sidebar/Sidebar.tsx | 3 - .../src/Sidebar/TreeView/SortableTreeItem.tsx | 7 +- .../app/src/Sidebar/TreeView/TreeView.tsx | 10 +- packages/dnd-projection/package.json | 23 ++ .../src/getProjection.tsx} | 3 +- packages/dnd-projection/src/index.ts | 1 + packages/dnd-projection/tsconfig.json | 8 + packages/editor-common/src/useEditor.ts | 12 + packages/editor/package.json | 2 + .../src/components/DragOverlayPreview.tsx | 21 ++ packages/editor/src/components/NodeEditor.tsx | 244 ++++++++++++++++-- .../src/components/ProtectionProvider.tsx | 18 ++ packages/model/src/Node.ts | 6 +- packages/service/src/NodeCleaner.ts | 3 +- packages/service/src/NodeListService.ts | 8 +- packages/service/src/NodeService/index.ts | 14 +- pnpm-lock.yaml | 52 ++++ 37 files changed, 511 insertions(+), 234 deletions(-) create mode 100644 extensions/list/src/hooks/useCollapsed.tsx rename extensions/list/src/{ => plugins}/withEditable.ts (100%) rename extensions/list/src/{ => plugins}/withListsPlugin.ts (68%) rename extensions/list/src/{ => plugins}/withMarkdown.ts (98%) create mode 100644 extensions/list/src/ui/GuideLine.tsx delete mode 100644 packages/app/src/Sidebar/NodeQuery/NodeItem.tsx delete mode 100644 packages/app/src/Sidebar/NodeQuery/NodeItemMenu.tsx delete mode 100644 packages/app/src/Sidebar/NodeQuery/NodeQuery.tsx delete mode 100644 packages/app/src/Sidebar/RecentlyEdited.tsx delete mode 100644 packages/app/src/Sidebar/RecentlyOpened.tsx create mode 100644 packages/dnd-projection/package.json rename packages/{app/src/Sidebar/TreeView/utils.ts => dnd-projection/src/getProjection.tsx} (95%) create mode 100644 packages/dnd-projection/src/index.ts create mode 100644 packages/dnd-projection/tsconfig.json create mode 100644 packages/editor/src/components/DragOverlayPreview.tsx create mode 100644 packages/editor/src/components/ProtectionProvider.tsx diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 90f3fc46..fcdffd87 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -54,6 +54,7 @@ const config = { '@penx/table', '@penx/database', '@penx/tag', + '@penx/dnd-projection', '@penx/block-selector', '@penx/editor-leaf', '@penx/trpc-client', diff --git a/extensions/list/package.json b/extensions/list/package.json index 0cf567be..2745e108 100644 --- a/extensions/list/package.json +++ b/extensions/list/package.json @@ -17,6 +17,9 @@ }, "dependencies": { "@bone-ui/utils": "^0.37.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.1.0", "@floating-ui/react": "^0.26.1", "@fower/react": "^2.0.0", "@penx/autoformat": "workspace:*", @@ -28,6 +31,7 @@ "@penx/local-db": "workspace:*", "@penx/remark-slate": "workspace:*", "@penx/serializer": "workspace:*", + "@penx/dnd-projection": "workspace:*", "@penx/store": "workspace:*", "@penx/types": "workspace:*", "date-fns": "^2.30.0", diff --git a/extensions/list/src/hooks/useCollapsed.tsx b/extensions/list/src/hooks/useCollapsed.tsx new file mode 100644 index 00000000..71d3ae6c --- /dev/null +++ b/extensions/list/src/hooks/useCollapsed.tsx @@ -0,0 +1,21 @@ +import { useMemo } from 'react' +import { Path } from 'slate' +import { useEditor } from '@penx/editor-common' +import { findNodePath, getNodeByPath } from '@penx/editor-queries' +import { isListContentElement } from '../guard' +import { ListElement } from '../types' + +export const useCollapsed = (element: ListElement) => { + const editor = useEditor() + const path = findNodePath(editor, element)! + + const collapsed = useMemo(() => { + if (path.length === 1) return false + const prevPath = Path.previous(path) + const node = getNodeByPath(editor, prevPath)! + if (isListContentElement(node)) return node.collapsed + return false + }, [path, editor]) + + return collapsed +} diff --git a/extensions/list/src/index.ts b/extensions/list/src/index.ts index a45fb897..3ee1b9e9 100644 --- a/extensions/list/src/index.ts +++ b/extensions/list/src/index.ts @@ -7,13 +7,13 @@ import { ELEMENT_UL, } from './constants' import { onKeyDown } from './onKeyDown' +import { withEditable } from './plugins/withEditable' +import { withListsPlugin } from './plugins/withListsPlugin' +import { withMarkdown } from './plugins/withMarkdown' import { List } from './ui/List' import { ListItem } from './ui/ListItem' import { ListItemContent } from './ui/ListItemContent' import { Title } from './ui/Title' -import { withEditable } from './withEditable' -import { withListsPlugin } from './withListsPlugin' -import { withMarkdown } from './withMarkdown' export * from './types' export * from './guard' diff --git a/extensions/list/src/withEditable.ts b/extensions/list/src/plugins/withEditable.ts similarity index 100% rename from extensions/list/src/withEditable.ts rename to extensions/list/src/plugins/withEditable.ts diff --git a/extensions/list/src/withListsPlugin.ts b/extensions/list/src/plugins/withListsPlugin.ts similarity index 68% rename from extensions/list/src/withListsPlugin.ts rename to extensions/list/src/plugins/withListsPlugin.ts index 89402ec2..18a77605 100644 --- a/extensions/list/src/withListsPlugin.ts +++ b/extensions/list/src/plugins/withListsPlugin.ts @@ -1,4 +1,4 @@ import { withLists } from 'slate-lists' -import { listSchema } from './listSchema' +import { listSchema } from '../listSchema' export const withListsPlugin = withLists(listSchema) diff --git a/extensions/list/src/withMarkdown.ts b/extensions/list/src/plugins/withMarkdown.ts similarity index 98% rename from extensions/list/src/withMarkdown.ts rename to extensions/list/src/plugins/withMarkdown.ts index 277df679..5837a657 100644 --- a/extensions/list/src/withMarkdown.ts +++ b/extensions/list/src/plugins/withMarkdown.ts @@ -4,7 +4,7 @@ import { unified } from 'unified' import { PenxEditor } from '@penx/editor-common' import { getCurrentPath } from '@penx/editor-queries' import slate from '@penx/remark-slate' -import { listSchema } from './listSchema' +import { listSchema } from '../listSchema' export const withMarkdown = (editor: PenxEditor) => { const { insertData } = editor diff --git a/extensions/list/src/ui/GuideLine.tsx b/extensions/list/src/ui/GuideLine.tsx new file mode 100644 index 00000000..66c7c0a4 --- /dev/null +++ b/extensions/list/src/ui/GuideLine.tsx @@ -0,0 +1,7 @@ +import { Box } from '@fower/react' + +export const GuideLine = () => { + return ( + + ) +} diff --git a/extensions/list/src/ui/List.tsx b/extensions/list/src/ui/List.tsx index 875dd4da..8d9c355e 100644 --- a/extensions/list/src/ui/List.tsx +++ b/extensions/list/src/ui/List.tsx @@ -1,10 +1,7 @@ -import { useMemo } from 'react' import { Box } from '@fower/react' -import { Path } from 'slate' import { useEditor } from '@penx/editor-common' -import { findNodePath, getNodeByPath } from '@penx/editor-queries' import { ElementProps } from '@penx/extension-typings' -import { isListContentElement } from '../guard' +import { useCollapsed } from '../hooks/useCollapsed' import { ListElement } from '../types' export const List = ({ @@ -14,15 +11,7 @@ export const List = ({ nodeProps, }: ElementProps) => { const editor = useEditor() - const path = findNodePath(editor, element)! - - const collapsed = useMemo(() => { - if (path.length === 1) return false - const prevPath = Path.previous(path) - const node = getNodeByPath(editor, prevPath)! - if (isListContentElement(node)) return node.collapsed - return false - }, [path, editor]) + const collapsed = useCollapsed(element) return ( 2} > - {path.length > 2 && ( - - )} + {path.length > 2 && } {children} ) diff --git a/extensions/list/src/ui/ListItemContent.tsx b/extensions/list/src/ui/ListItemContent.tsx index 238a533c..d5d2edd0 100644 --- a/extensions/list/src/ui/ListItemContent.tsx +++ b/extensions/list/src/ui/ListItemContent.tsx @@ -1,3 +1,7 @@ +import React, { CSSProperties, useState } from 'react' +import { mergeRefs } from '@bone-ui/utils' +import { AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import { Box } from '@fower/react' import { Path, Transforms } from 'slate' import { ContextMenu, MenuItem, useContextMenu } from '@penx/context-menu' @@ -8,6 +12,11 @@ import { ListContentElement } from '../types' import { Bullet } from './Bullet' import { Chevron } from './Chevron' +const animateLayoutChanges: AnimateLayoutChanges = ({ + isSorting, + wasDragging, +}) => (isSorting || wasDragging ? false : true) + export const ListItemContent = ({ attributes, element, @@ -31,10 +40,67 @@ export const ListItemContent = ({ Transforms.removeNodes(editor, { at: Path.parent(path) }) } } + const { id } = element + + const sortable = useSortable({ + id: id, + animateLayoutChanges, + }) + + const { + over, + active, + overIndex, + activeIndex, + isDragging, + isSorting, + items, + data, + isOver, + listeners, + setDraggableNodeRef, + setDroppableNodeRef, + setNodeRef, + transform, + transition, + } = sortable + + function getActiveStyle() { + if (!over || !active) return {} + if (id !== over.id) return {} + + const node = editor.flattenedItems.find(({ id }) => id === element.id) + + const { projected } = editor + + const isNextDepth = projected?.depth !== node?.depth + + const isAfter = overIndex > activeIndex + const style = { + left: isNextDepth ? 20 : -10, + // left: -10, + right: 0, + top0: !isAfter, + bottom0: isAfter, + content: '""', + position: 'absolute', + h: 3, + bgBrand300: true, + } + return { + '::after': style, + } + } + + const style: CSSProperties = { + transform: isSorting ? undefined : CSS.Translate.toString(transform), + transition, + } return ( handleItemClick('f')}>Collapse all - + + + + diff --git a/packages/app/package.json b/packages/app/package.json index 5e18da44..b77008d6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -58,6 +58,7 @@ "@penx/constants": "workspace:*", "@penx/db": "workspace:*", "@penx/remark-slate": "workspace:*", + "@penx/dnd-projection": "workspace:*", "@penx/editor": "workspace:*", "@penx/editor-common": "workspace:*", "@penx/editor-queries": "workspace:*", diff --git a/packages/app/src/EditorLayout/EditorLayout.tsx b/packages/app/src/EditorLayout/EditorLayout.tsx index c3ab8df9..c9f140cb 100644 --- a/packages/app/src/EditorLayout/EditorLayout.tsx +++ b/packages/app/src/EditorLayout/EditorLayout.tsx @@ -22,7 +22,7 @@ export const EditorLayout: FC = ({ children }) => { if (!spaces?.length) return null - console.log('router name==========:', name) + // console.log('router name==========:', name) return ( diff --git a/packages/app/src/EditorLayout/SidebarDrawer.tsx b/packages/app/src/EditorLayout/SidebarDrawer.tsx index afee9663..3783cfb7 100644 --- a/packages/app/src/EditorLayout/SidebarDrawer.tsx +++ b/packages/app/src/EditorLayout/SidebarDrawer.tsx @@ -5,7 +5,6 @@ import { Button } from 'uikit' import { useSidebarDrawer, useUser } from '@penx/hooks' import { store } from '@penx/store' import { FavoriteBox } from '../Sidebar/FavoriteBox/FavoriteBox' -import { RecentlyEdited } from '../Sidebar/RecentlyEdited' import { SidebarItem } from '../Sidebar/SidebarItem' import { SpacePopover } from '../Sidebar/SpacePopover' @@ -85,7 +84,6 @@ export const DrawerSidebar = () => { }} /> - Address: {user?.address} diff --git a/packages/app/src/NodeContent.tsx b/packages/app/src/NodeContent.tsx index 63456b34..616d101c 100644 --- a/packages/app/src/NodeContent.tsx +++ b/packages/app/src/NodeContent.tsx @@ -29,12 +29,15 @@ export function NodeContent() { if (!node.id || !nodes.length) return null + // console.log('nodes=========:', nodes) + return ( { if (isAstChange(editor)) { debouncedSaveNodes(value) diff --git a/packages/app/src/Sidebar/NodeQuery/NodeItem.tsx b/packages/app/src/Sidebar/NodeQuery/NodeItem.tsx deleted file mode 100644 index bd65ed69..00000000 --- a/packages/app/src/Sidebar/NodeQuery/NodeItem.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { memo } from 'react' -import { Box } from '@fower/react' -import { useSidebarDrawer } from '@penx/hooks' -import { Node } from '@penx/model' -import { NodeService } from '@penx/service' -import { store } from '@penx/store' -import { NodeItemMenu } from './NodeItemMenu' - -interface Props { - node: Node -} - -export const NodeItem = memo( - function NodeItem({ node }: Props) { - const { close } = useSidebarDrawer() - - return ( - { - const nodeService = new NodeService( - node, - store.getNodes().map((n) => new Node(n)), - ) - nodeService.selectNode() - close?.() - }} - > - {node.title || 'Untitled'} - e.stopPropagation()}> - - - - ) - }, - (prevProps, nextProps) => { - return ( - prevProps.node.id === nextProps.node.id && - prevProps.node.title === nextProps.node.title - ) - }, -) diff --git a/packages/app/src/Sidebar/NodeQuery/NodeItemMenu.tsx b/packages/app/src/Sidebar/NodeQuery/NodeItemMenu.tsx deleted file mode 100644 index 53aaaf58..00000000 --- a/packages/app/src/Sidebar/NodeQuery/NodeItemMenu.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FC } from 'react' -import { Box } from '@fower/react' -import { MoreHorizontal, Trash } from 'lucide-react' -import { - MenuItem, - Popover, - PopoverClose, - PopoverContent, - PopoverTrigger, -} from 'uikit' -import { Node } from '@penx/model' - -interface Props { - node: Node -} - -export const NodeItemMenu: FC = ({ node }) => { - return ( - - - - - - - - - - {}}> - - Delete - - - - - - ) -} diff --git a/packages/app/src/Sidebar/NodeQuery/NodeQuery.tsx b/packages/app/src/Sidebar/NodeQuery/NodeQuery.tsx deleted file mode 100644 index b48ad2c4..00000000 --- a/packages/app/src/Sidebar/NodeQuery/NodeQuery.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Box } from '@fower/react' -import { MoreHorizontal } from 'lucide-react' -import { Button } from 'uikit' -import { useNodes, useSpaces } from '@penx/hooks' -import { SqlParser } from '../SqlParser' -import { NodeItem } from './NodeItem' - -interface Props { - sql: string - title: string -} - -export const NodeQuery = ({ sql, title }: Props) => { - const { nodeList } = useNodes() - const { activeSpace } = useSpaces() - const parsed = new SqlParser(sql) - - // const nodes = nodeList.find({ - // where: { - // spaceId: activeSpace.id, - // }, - // ...parsed.queryParams, - // }) - const nodes = nodeList.rootNodes - - return ( - - - {title} - - - - {nodes.map((node) => ( - - ))} - - - ) -} diff --git a/packages/app/src/Sidebar/RecentlyEdited.tsx b/packages/app/src/Sidebar/RecentlyEdited.tsx deleted file mode 100644 index 4be253b6..00000000 --- a/packages/app/src/Sidebar/RecentlyEdited.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { NodeQuery } from './NodeQuery/NodeQuery' - -const sql = 'SELECT * FROM node ORDER BY updatedAt DESC limit 20' - -export const RecentlyEdited = () => { - return -} diff --git a/packages/app/src/Sidebar/RecentlyOpened.tsx b/packages/app/src/Sidebar/RecentlyOpened.tsx deleted file mode 100644 index 22ee17a5..00000000 --- a/packages/app/src/Sidebar/RecentlyOpened.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { NodeQuery } from './NodeQuery/NodeQuery' - -const sql = 'SELECT * FROM doc ORDER BY openedAt DESC limit 4' - -export const RecentlyOpened = () => { - return -} diff --git a/packages/app/src/Sidebar/Sidebar.tsx b/packages/app/src/Sidebar/Sidebar.tsx index 357a6319..de3e2b7b 100644 --- a/packages/app/src/Sidebar/Sidebar.tsx +++ b/packages/app/src/Sidebar/Sidebar.tsx @@ -6,7 +6,6 @@ import { useNodes } from '@penx/hooks' import { extensionStoreAtom, store } from '@penx/store' import { ExtensionStore } from '@penx/types' import { FavoriteBox } from './FavoriteBox/FavoriteBox' -import { RecentlyEdited } from './RecentlyEdited' import { SidebarItem } from './SidebarItem' import { SpacePopover } from './SpacePopover' import { TreeView } from './TreeView/TreeView' @@ -96,8 +95,6 @@ export const Sidebar = () => { {!!nodes.length && } - - } label="Trash" diff --git a/packages/app/src/Sidebar/TreeView/SortableTreeItem.tsx b/packages/app/src/Sidebar/TreeView/SortableTreeItem.tsx index b07c3103..d0572153 100644 --- a/packages/app/src/Sidebar/TreeView/SortableTreeItem.tsx +++ b/packages/app/src/Sidebar/TreeView/SortableTreeItem.tsx @@ -2,8 +2,7 @@ import React, { CSSProperties, useState } from 'react' import { AnimateLayoutChanges, useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { CSSObject } from '@fower/react' -import { Node } from '@penx/model' -import { WithFlattenedProps } from '@penx/service' +import { Node, WithFlattenedProps } from '@penx/model' import { TreeItem } from './TreeItem' interface Props { @@ -55,9 +54,9 @@ export function SortableTreeItem({ node, level, overDepth }: Props) { bottom0: isAfter, content: '""', position: 'absolute', - h: 2, + h: 3, // w: '100%', - bgBrand500: true, + bgBrand300: true, } return { '::after': style, diff --git a/packages/app/src/Sidebar/TreeView/TreeView.tsx b/packages/app/src/Sidebar/TreeView/TreeView.tsx index 4ca4244d..aac5c507 100644 --- a/packages/app/src/Sidebar/TreeView/TreeView.tsx +++ b/packages/app/src/Sidebar/TreeView/TreeView.tsx @@ -27,13 +27,13 @@ import { } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Box } from '@fower/react' +import { getProjection, UniqueIdentifier } from '@penx/dnd-projection' import { useNodes } from '@penx/hooks' import { db } from '@penx/local-db' import { NodeCleaner, NodeListService } from '@penx/service' import { store } from '@penx/store' import { SortableTreeItem } from './SortableTreeItem' import { TreeItem } from './TreeItem' -import { getProjection, UniqueIdentifier } from './utils' const measuring = { droppable: { @@ -149,8 +149,8 @@ export const TreeView = ({ nodeList }: TreeViewProps) => { sensors={sensors} collisionDetection={closestCenter} measuring={measuring} - onDragMove={handleDragMove} onDragStart={handleDragStart} + onDragMove={handleDragMove} onDragOver={handleDragOver} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} @@ -188,6 +188,7 @@ export const TreeView = ({ nodeList }: TreeViewProps) => { document.body.style.setProperty('cursor', 'grabbing') } + function handleDragMove({ delta }: DragMoveEvent) { setOffsetLeft(delta.x) } @@ -205,7 +206,7 @@ export const TreeView = ({ nodeList }: TreeViewProps) => { if (!(overId && projected)) return const { depth, parentId } = projected - console.log('gogo........: ', depth, 'parentId:', parentId) + // console.log('handleDragEnd======: ', depth, 'parentId:', parentId) if (!parentId) { if (activeId !== overId) { @@ -284,7 +285,8 @@ export const TreeView = ({ nodeList }: TreeViewProps) => { parentId: activeNode.raw.parentId, }), ]) - console.log('activeNode.parentId,:', activeNode.parentId) + + // console.log('activeNode.parentId,:', activeNode.parentId) await new NodeCleaner().cleanDeletedNodes() } diff --git a/packages/dnd-projection/package.json b/packages/dnd-projection/package.json new file mode 100644 index 00000000..0423f17b --- /dev/null +++ b/packages/dnd-projection/package.json @@ -0,0 +1,23 @@ +{ + "name": "@penx/dnd-projection", + "version": "0.0.0", + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "gen": "tsx generator.ts", + "lint": "eslint \"**/*.ts*\"" + }, + "devDependencies": { + "@types/react": "^18.2.22", + "@types/react-dom": "^18.2.7", + "eslint": "^8.42.0", + "eslint-config-custom": "workspace:*", + "react": "^18.2.0", + "tsconfig": "workspace:*", + "typescript": "^5.1.3" + }, + "dependencies": { + "@dnd-kit/sortable": "^7.0.2", + "@penx/model": "workspace:*" + } +} diff --git a/packages/app/src/Sidebar/TreeView/utils.ts b/packages/dnd-projection/src/getProjection.tsx similarity index 95% rename from packages/app/src/Sidebar/TreeView/utils.ts rename to packages/dnd-projection/src/getProjection.tsx index 537450da..2d675823 100644 --- a/packages/app/src/Sidebar/TreeView/utils.ts +++ b/packages/dnd-projection/src/getProjection.tsx @@ -1,6 +1,5 @@ import { arrayMove } from '@dnd-kit/sortable' -import { Node } from '@penx/model' -import { WithFlattenedProps } from '@penx/service' +import { Node, WithFlattenedProps } from '@penx/model' export type UniqueIdentifier = string diff --git a/packages/dnd-projection/src/index.ts b/packages/dnd-projection/src/index.ts new file mode 100644 index 00000000..3fec9d9e --- /dev/null +++ b/packages/dnd-projection/src/index.ts @@ -0,0 +1 @@ +export * from './getProjection' diff --git a/packages/dnd-projection/tsconfig.json b/packages/dnd-projection/tsconfig.json new file mode 100644 index 00000000..72024d2c --- /dev/null +++ b/packages/dnd-projection/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "tsconfig/react-library.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"] + }, + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/editor-common/src/useEditor.ts b/packages/editor-common/src/useEditor.ts index 470ab258..0cbaefae 100644 --- a/packages/editor-common/src/useEditor.ts +++ b/packages/editor-common/src/useEditor.ts @@ -4,6 +4,13 @@ import { ReactEditor, useSlate, useSlateStatic } from 'slate-react' export type TElement = Element & { type: T; nodeType?: string } +export type Projected = { + depth: number + maxDepth: number + minDepth: number + parentId: string | null +} + export type PenxEditor = BaseEditor & ReactEditor & HistoryEditor & { @@ -14,6 +21,11 @@ export type PenxEditor = BaseEditor & isBlockSelectorOpened: boolean isTagSelectorOpened: boolean nodeToDecorations: Map + + projected: Projected | null + + // save flattened node to editor + flattenedItems: any[] } export function useEditor() { diff --git a/packages/editor/package.json b/packages/editor/package.json index 5125af9f..f3dfc6c6 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -33,6 +33,8 @@ "@fower/react": "^2.0.0", "@penx/autoformat": "workspace:*", "@penx/model": "workspace:*", + "@penx/service": "workspace:*", + "@penx/dnd-projection": "workspace:*", "@penx/editor-leaf": "workspace:*", "@penx/editor-common": "workspace:*", "@penx/editor-queries": "workspace:*", diff --git a/packages/editor/src/components/DragOverlayPreview.tsx b/packages/editor/src/components/DragOverlayPreview.tsx new file mode 100644 index 00000000..d6a6e86e --- /dev/null +++ b/packages/editor/src/components/DragOverlayPreview.tsx @@ -0,0 +1,21 @@ +import { Box } from '@fower/react' + +export function DragOverlayPreview() { + return ( + + + + ) +} diff --git a/packages/editor/src/components/NodeEditor.tsx b/packages/editor/src/components/NodeEditor.tsx index 253aae8e..102fc092 100644 --- a/packages/editor/src/components/NodeEditor.tsx +++ b/packages/editor/src/components/NodeEditor.tsx @@ -1,29 +1,103 @@ -import { FocusEvent, KeyboardEvent, useCallback } from 'react' -import { css } from '@fower/react' -import { Descendant, Editor } from 'slate' +import { + FocusEvent, + KeyboardEvent, + useCallback, + useMemo, + useState, +} from 'react' +import { createPortal } from 'react-dom' +import { + closestCenter, + defaultDropAnimation, + DndContext, + DragEndEvent, + DragMoveEvent, + DragOverEvent, + DragOverlay, + DragStartEvent, + DropAnimation, + KeyboardSensor, + MeasuringStrategy, + Modifier, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { + rectSortingStrategy, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { Box, css } from '@fower/react' +import { Descendant, Editor, Path, Transforms } from 'slate' import { Editable, RenderElementProps, Slate } from 'slate-react' import { EditableProps } from 'slate-react/dist/components/editable' import { onKeyDownAutoformat } from '@penx/autoformat' import { SetNodeToDecorations } from '@penx/code-block' +import { getProjection } from '@penx/dnd-projection' import { Leaf } from '@penx/editor-leaf' -import { useExtensionStore } from '@penx/hooks' +import { useExtensionStore, useNodes } from '@penx/hooks' +import { Node } from '@penx/model' import { useCreateEditor } from '../hooks/useCreateEditor' import { useDecorate } from '../hooks/useDecorate' import { useOnCompositionEvent } from '../hooks/useOnCompositionEvent' import { useOnDOMBeforeInput } from '../hooks/useOnDOMBeforeInput' import ClickablePadding from './ClickablePadding' +import { DragOverlayPreview } from './DragOverlayPreview' import { ElementContent } from './ElementContent' import HoveringToolbar from './HoveringToolbar/HoveringToolbar' +import { ProtectionProvider } from './ProtectionProvider' interface Props { content: any[] + node: Node editableProps?: EditableProps plugins: ((editor: Editor) => Editor)[] onChange?: (value: Descendant[], editor: Editor) => void onBlur?: (editor: Editor) => void } -export function NodeEditor({ content, onChange, onBlur, plugins }: Props) { +const measuring = { + droppable: { + strategy: MeasuringStrategy.Always, + }, +} + +const dropAnimationConfig: DropAnimation = { + keyframes({ transform }) { + return [ + { opacity: 1, transform: CSS.Transform.toString(transform.initial) }, + { + opacity: 0, + transform: CSS.Transform.toString({ + ...transform.final, + x: transform.final.x + 5, + y: transform.final.y + 5, + }), + }, + ] + }, + easing: 'ease-out', + sideEffects({ active }) { + active.node.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: defaultDropAnimation.duration, + easing: defaultDropAnimation.easing, + }) + }, +} + +export type UniqueIdentifier = string + +export function NodeEditor({ + content, + node, + onChange, + onBlur, + plugins, +}: Props) { + const { nodeList } = useNodes() const editor = useCreateEditor(plugins) const { extensionStore } = useExtensionStore() const decorate = useDecorate(editor) @@ -60,6 +134,48 @@ export function NodeEditor({ content, onChange, onBlur, plugins }: Props) { } } + const indentationWidth = 50 + const [activeId, setActiveId] = useState(null) + const [overId, setOverId] = useState(null) + const [offsetLeft, setOffsetLeft] = useState(0) + + const flattenedItems = useMemo(() => { + return nodeList.flattenNode(node) + }, [nodeList, node]) + + editor.flattenedItems = flattenedItems + + const projected = + activeId && overId + ? getProjection( + flattenedItems, + activeId, + overId, + offsetLeft, + indentationWidth, + ) + : null + + // save projection to editor + // TODO: not use projected now, do it later + editor.projected = projected + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + delay: 100, + tolerance: 5, + }, + }), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ) + + const activeItem = activeId + ? flattenedItems.find(({ id }) => id === activeId) + : null + return ( - } - renderElement={renderElement} - decorate={decorate as any} // - onCompositionUpdate={onOnCompositionEvent} - onCompositionEnd={onOnCompositionEvent} - onKeyDown={keyDown} - onDOMBeforeInput={onDOMBeforeInput} - onBlur={blur} - /> + + + + + } + renderElement={renderElement} + decorate={decorate as any} // + onCompositionUpdate={onOnCompositionEvent} + onCompositionEnd={onOnCompositionEvent} + onKeyDown={keyDown} + onDOMBeforeInput={onDOMBeforeInput} + onBlur={blur} + /> + + {createPortal( + + {activeId && activeItem ? : null} + , + document.body, + )} + + + ) + + function handleDragStart({ active: { id: activeId } }: DragStartEvent) { + setActiveId(activeId as string) + setOverId(activeId as string) + + document.body.style.setProperty('cursor', 'grabbing') + } + + function handleDragMove({ delta }: DragMoveEvent) { + setOffsetLeft(delta.x) + } + + function handleDragOver({ over }: DragOverEvent) { + setOverId((over?.id as any) ?? null) + } + + function handleDragEnd({ active, over }: DragEndEvent) { + resetState() + + const activeId = active.id as string + const overId = over?.id as string + + if (!(overId && projected)) return + console.log('protected============:', projected) + + if (activeId === overId) { + console.log('same........') + // TODO: + return + } + + console.log('overID:', overId) + + const [activeEntry] = Editor.nodes(editor, { + at: [], + match: (n: any) => n.id === activeId, + }) + + const [overEntry] = Editor.nodes(editor, { + at: [], + match: (n: any) => n.id === overId, + }) + + if (overEntry) { + Transforms.moveNodes(editor, { + at: Path.parent(activeEntry[1]), + // match: (n: any) => n === activeEntry[0], + to: Path.parent(overEntry[1]), + }) + + console.log('entry:', overEntry) + } + + // console.log('handleDragEnd======: ', depth, 'parentId:', parentId) + } + + function handleDragCancel() { + resetState() + } + + function resetState() { + setActiveId(null) + setOverId(null) + setOffsetLeft(0) + document.body.style.setProperty('cursor', '') + } } diff --git a/packages/editor/src/components/ProtectionProvider.tsx b/packages/editor/src/components/ProtectionProvider.tsx new file mode 100644 index 00000000..bda949be --- /dev/null +++ b/packages/editor/src/components/ProtectionProvider.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react' + +export interface ProtectionContext { + depth: number + maxDepth: number + minDepth: number + parentId: string +} + +export const protectionContext = createContext( + {} as ProtectionContext, +) + +export const ProtectionProvider = protectionContext.Provider + +export function useProtectionContext() { + return useContext(protectionContext) +} diff --git a/packages/model/src/Node.ts b/packages/model/src/Node.ts index a503c480..2bd512d5 100644 --- a/packages/model/src/Node.ts +++ b/packages/model/src/Node.ts @@ -8,7 +8,11 @@ type Element = { children: Array<{ text: string }> } -export const isRootNode = () => {} +export type WithFlattenedProps = T & { + parentId: string | null // parent node id + depth: number + index: number +} export class Node { constructor(public raw: INode) {} diff --git a/packages/service/src/NodeCleaner.ts b/packages/service/src/NodeCleaner.ts index e8b4548a..08c14cbb 100644 --- a/packages/service/src/NodeCleaner.ts +++ b/packages/service/src/NodeCleaner.ts @@ -32,8 +32,7 @@ export class NodeCleaner { const parentNode = nodeMap.get(node.parentId) if (!parentNode?.children.includes(node.id)) { - console.log('clear node!!!!', node) - + console.log('=======clear node!!!!', node) await db.deleteNode(node.id) } } diff --git a/packages/service/src/NodeListService.ts b/packages/service/src/NodeListService.ts index 67e1d9a9..d10b7187 100644 --- a/packages/service/src/NodeListService.ts +++ b/packages/service/src/NodeListService.ts @@ -1,14 +1,8 @@ import _ from 'lodash' import { ArraySorter } from '@penx/indexeddb' -import { Node } from '@penx/model' +import { Node, WithFlattenedProps } from '@penx/model' import { INode, NodeType } from '@penx/types' -export type WithFlattenedProps = T & { - parentId: string | null // parent node id - depth: number - index: number -} - export type FindOptions = { where?: Partial limit?: number diff --git a/packages/service/src/NodeService/index.ts b/packages/service/src/NodeService/index.ts index 63efbd28..276001a3 100644 --- a/packages/service/src/NodeService/index.ts +++ b/packages/service/src/NodeService/index.ts @@ -59,7 +59,10 @@ export class NodeService { return getDatabaseRootEditorValue(this.node, this.nodeMap) } - const childrenToList = (children: string[]) => { + const childrenToList = ( + children: string[], + parentId: string | null = null, + ) => { const listItems = children.map((id) => { const node = this.nodeMap.get(id)! @@ -68,13 +71,14 @@ export class NodeService { id: node.id, type: ELEMENT_LIC, nodeType: node.type, + parentId, collapsed: node.collapsed, children: [node.element], }, ] if (node.children) { - const ul = childrenToList(node.children) + const ul = childrenToList(node.children, node.id) if (ul) children.push(ul as any) } @@ -103,6 +107,7 @@ export class NodeService { { id: node.id, type: ELEMENT_LIC, + parentId: null, nodeType: node.type, props: node.props, collapsed: node.collapsed, @@ -110,7 +115,7 @@ export class NodeService { }, ] - const ul = childrenToList(node.children) as any + const ul = childrenToList(node.children, node.id) as any if (ul) listChildren.push(ul) return { @@ -270,6 +275,7 @@ export class NodeService { if (parent.children.length > 1) { const listItems = parent.children[1] .children as any as ListItemElement[] + children = listItems.map((item) => { return item.children[0].id }) @@ -325,7 +331,7 @@ export class NodeService { } private extractTags(element: TElement) { - // console.log('element===:', element) + if (!element.children) return [] return element.children .filter((item: any) => item.type === 'tag') .map((i: any) => i.name.replace('#', '')) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e938fa31..58d287ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1763,6 +1763,15 @@ importers: '@bone-ui/utils': specifier: ^0.37.0 version: 0.37.0 + '@dnd-kit/core': + specifier: ^6.0.8 + version: 6.0.8(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': + specifier: ^7.0.2 + version: 7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0) + '@dnd-kit/utilities': + specifier: ^3.1.0 + version: 3.2.1(react@18.2.0) '@floating-ui/react': specifier: ^0.26.1 version: 0.26.1(react-dom@18.2.0)(react@18.2.0) @@ -1778,6 +1787,9 @@ importers: '@penx/context-menu': specifier: workspace:* version: link:../../packages/context-menu + '@penx/dnd-projection': + specifier: workspace:* + version: link:../../packages/dnd-projection '@penx/editor-common': specifier: workspace:* version: link:../../packages/editor-common @@ -2406,6 +2418,9 @@ importers: '@penx/db': specifier: workspace:* version: link:../db + '@penx/dnd-projection': + specifier: workspace:* + version: link:../dnd-projection '@penx/editor': specifier: workspace:* version: link:../editor @@ -2874,6 +2889,37 @@ importers: specifier: ^5.1.3 version: 5.2.2 + packages/dnd-projection: + dependencies: + '@dnd-kit/sortable': + specifier: ^7.0.2 + version: 7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0) + '@penx/model': + specifier: workspace:* + version: link:../model + devDependencies: + '@types/react': + specifier: ^18.2.22 + version: 18.2.22 + '@types/react-dom': + specifier: ^18.2.7 + version: 18.2.7 + eslint: + specifier: ^8.42.0 + version: 8.49.0 + eslint-config-custom: + specifier: workspace:* + version: link:../eslint-config-custom + react: + specifier: ^18.2.0 + version: 18.2.0 + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: ^5.1.3 + version: 5.2.2 + packages/easy-modal: dependencies: immer: @@ -2934,6 +2980,9 @@ importers: '@penx/code-block': specifier: workspace:* version: link:../../extensions/code-block + '@penx/dnd-projection': + specifier: workspace:* + version: link:../dnd-projection '@penx/editor-common': specifier: workspace:* version: link:../editor-common @@ -2976,6 +3025,9 @@ importers: '@penx/paragraph': specifier: workspace:* version: link:../../extensions/paragraph + '@penx/service': + specifier: workspace:* + version: link:../service '@penx/shared': specifier: workspace:* version: link:../shared