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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
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