mirror of
https://github.com/penxio/penx.git
synced 2026-04-19 03:03:06 -04:00
feat: can dnd in List
This commit is contained in:
@@ -54,6 +54,7 @@ const config = {
|
||||
'@penx/table',
|
||||
'@penx/database',
|
||||
'@penx/tag',
|
||||
'@penx/dnd-projection',
|
||||
'@penx/block-selector',
|
||||
'@penx/editor-leaf',
|
||||
'@penx/trpc-client',
|
||||
|
||||
@@ -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",
|
||||
|
||||
21
extensions/list/src/hooks/useCollapsed.tsx
Normal file
21
extensions/list/src/hooks/useCollapsed.tsx
Normal file
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { withLists } from 'slate-lists'
|
||||
import { listSchema } from './listSchema'
|
||||
import { listSchema } from '../listSchema'
|
||||
|
||||
export const withListsPlugin = withLists(listSchema)
|
||||
@@ -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
|
||||
7
extensions/list/src/ui/GuideLine.tsx
Normal file
7
extensions/list/src/ui/GuideLine.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Box } from '@fower/react'
|
||||
|
||||
export const GuideLine = () => {
|
||||
return (
|
||||
<Box contentEditable={false} absolute left--40 top0 bottom0 bgGray200 w-1 />
|
||||
)
|
||||
}
|
||||
@@ -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<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])
|
||||
const collapsed = useCollapsed(element)
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSlateStatic } from 'slate-react'
|
||||
import { findNodePath } from '@penx/editor-queries'
|
||||
import { ElementProps } from '@penx/extension-typings'
|
||||
import { ListItemElement } from '../types'
|
||||
import { GuideLine } from './GuideLine'
|
||||
|
||||
export const ListItem = ({
|
||||
attributes,
|
||||
@@ -22,17 +23,7 @@ export const ListItem = ({
|
||||
relative
|
||||
pl0={path.length > 2}
|
||||
>
|
||||
{path.length > 2 && (
|
||||
<Box
|
||||
contentEditable={false}
|
||||
absolute
|
||||
left--40
|
||||
top0
|
||||
bottom0
|
||||
bgGray200
|
||||
w-1
|
||||
/>
|
||||
)}
|
||||
{path.length > 2 && <GuideLine />}
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
{...attributes}
|
||||
ref={mergeRefs([sortable.setNodeRef, attributes.ref])}
|
||||
data-type="list-item-content"
|
||||
m0
|
||||
leadingNormal
|
||||
@@ -44,6 +110,8 @@ export const ListItemContent = ({
|
||||
p1
|
||||
gap2
|
||||
{...nodeProps}
|
||||
css={getActiveStyle()}
|
||||
style={style}
|
||||
className="nodeContent"
|
||||
>
|
||||
<Box
|
||||
@@ -75,7 +143,10 @@ export const ListItemContent = ({
|
||||
<MenuItem onClick={() => handleItemClick('f')}>Collapse all</MenuItem>
|
||||
</ContextMenu>
|
||||
<Chevron element={element} onContextMenu={show} />
|
||||
<Bullet element={element} onContextMenu={show} />
|
||||
|
||||
<Box inlineFlex {...sortable.listeners}>
|
||||
<Bullet element={element} onContextMenu={show} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flex-1 pl1 leadingNormal>
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -22,7 +22,7 @@ export const EditorLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
|
||||
if (!spaces?.length) return null
|
||||
|
||||
console.log('router name==========:', name)
|
||||
// console.log('router name==========:', name)
|
||||
|
||||
return (
|
||||
<EditorProvider space={activeSpace}>
|
||||
|
||||
@@ -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 = () => {
|
||||
}}
|
||||
/>
|
||||
<FavoriteBox />
|
||||
<RecentlyEdited />
|
||||
</Box>
|
||||
<Box>Address: {user?.address}</Box>
|
||||
</Box>
|
||||
|
||||
@@ -29,12 +29,15 @@ export function NodeContent() {
|
||||
|
||||
if (!node.id || !nodes.length) return null
|
||||
|
||||
// console.log('nodes=========:', nodes)
|
||||
|
||||
return (
|
||||
<Box relative>
|
||||
<Box mx-auto maxW-800>
|
||||
<NodeEditor
|
||||
plugins={[listPlugin]}
|
||||
content={nodeService.getEditorValue()}
|
||||
node={node}
|
||||
onChange={(value, editor) => {
|
||||
if (isAstChange(editor)) {
|
||||
debouncedSaveNodes(value)
|
||||
|
||||
@@ -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 (
|
||||
<Box
|
||||
className="nodeItem"
|
||||
toCenterY
|
||||
gap2
|
||||
gray500
|
||||
textSM
|
||||
h-24
|
||||
px1
|
||||
bgGray100--hover
|
||||
cursorPointer
|
||||
rounded
|
||||
onClick={() => {
|
||||
const nodeService = new NodeService(
|
||||
node,
|
||||
store.getNodes().map((n) => new Node(n)),
|
||||
)
|
||||
nodeService.selectNode()
|
||||
close?.()
|
||||
}}
|
||||
>
|
||||
<Box flex-1>{node.title || 'Untitled'}</Box>
|
||||
<Box inlineFlex onClick={(e) => e.stopPropagation()}>
|
||||
<NodeItemMenu node={node} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.node.id === nextProps.node.id &&
|
||||
prevProps.node.title === nextProps.node.title
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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<Props> = ({ node }) => {
|
||||
return (
|
||||
<Popover placement="right-start">
|
||||
<PopoverTrigger asChild>
|
||||
<Box
|
||||
inlineFlex
|
||||
bgGray200--hover
|
||||
rounded
|
||||
cursorPointer
|
||||
gray700
|
||||
p-2
|
||||
opacity-0
|
||||
opacity-100--$nodeItem--hover
|
||||
>
|
||||
<MoreHorizontal size={18} />
|
||||
</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w-200 textSM>
|
||||
<Box>
|
||||
<PopoverClose asChild>
|
||||
<MenuItem gap2 onClick={async () => {}}>
|
||||
<Trash size={18} />
|
||||
<Box>Delete</Box>
|
||||
</MenuItem>
|
||||
</PopoverClose>
|
||||
</Box>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<Box gray600 px3 py1 rounded2XL mb3>
|
||||
<Box toCenterY toBetween gap2>
|
||||
<Box fontBold>{title}</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="gray700"
|
||||
isSquare
|
||||
roundedFull
|
||||
>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</Box>
|
||||
<Box column>
|
||||
{nodes.map((node) => (
|
||||
<NodeItem key={node.id} node={node} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { NodeQuery } from './NodeQuery/NodeQuery'
|
||||
|
||||
const sql = 'SELECT * FROM node ORDER BY updatedAt DESC limit 20'
|
||||
|
||||
export const RecentlyEdited = () => {
|
||||
return <NodeQuery sql={sql} title="Recently Edited" />
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { NodeQuery } from './NodeQuery/NodeQuery'
|
||||
|
||||
const sql = 'SELECT * FROM doc ORDER BY openedAt DESC limit 4'
|
||||
|
||||
export const RecentlyOpened = () => {
|
||||
return <NodeQuery sql={sql} title="Recently Opened" />
|
||||
}
|
||||
@@ -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 && <TreeView nodeList={nodeList} />}
|
||||
|
||||
<RecentlyEdited />
|
||||
|
||||
<SidebarItem
|
||||
icon={<Trash2 size={16} />}
|
||||
label="Trash"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
23
packages/dnd-projection/package.json
Normal file
23
packages/dnd-projection/package.json
Normal file
@@ -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:*"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
1
packages/dnd-projection/src/index.ts
Normal file
1
packages/dnd-projection/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './getProjection'
|
||||
8
packages/dnd-projection/tsconfig.json
Normal file
8
packages/dnd-projection/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext", "DOM"]
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
||||
@@ -4,6 +4,13 @@ import { ReactEditor, useSlate, useSlateStatic } from 'slate-react'
|
||||
|
||||
export type TElement<T = string> = 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<any, Range[]>
|
||||
|
||||
projected: Projected | null
|
||||
|
||||
// save flattened node to editor
|
||||
flattenedItems: any[]
|
||||
}
|
||||
|
||||
export function useEditor() {
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
21
packages/editor/src/components/DragOverlayPreview.tsx
Normal file
21
packages/editor/src/components/DragOverlayPreview.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Box } from '@fower/react'
|
||||
|
||||
export function DragOverlayPreview() {
|
||||
return (
|
||||
<Box
|
||||
className="bullet"
|
||||
square-15
|
||||
bgTransparent
|
||||
bgGray200--hover
|
||||
bgGray200
|
||||
toCenter
|
||||
roundedFull
|
||||
ml--14
|
||||
mt-6
|
||||
cursorPointer
|
||||
flexShrink-1
|
||||
>
|
||||
<Box square-5 bgGray400 roundedFull transitionCommon scale-130 />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -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<UniqueIdentifier | null>(null)
|
||||
const [overId, setOverId] = useState<UniqueIdentifier | null>(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 (
|
||||
<Slate
|
||||
editor={editor}
|
||||
@@ -70,18 +186,114 @@ export function NodeEditor({ content, onChange, onBlur, plugins }: Props) {
|
||||
>
|
||||
<HoveringToolbar />
|
||||
<SetNodeToDecorations />
|
||||
<Editable
|
||||
className={css('black mt4 outlineNone')}
|
||||
renderLeaf={(props) => <Leaf {...props} />}
|
||||
renderElement={renderElement}
|
||||
decorate={decorate as any} //
|
||||
onCompositionUpdate={onOnCompositionEvent}
|
||||
onCompositionEnd={onOnCompositionEvent}
|
||||
onKeyDown={keyDown}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
onBlur={blur}
|
||||
/>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
measuring={measuring}
|
||||
onDragStart={handleDragStart}
|
||||
onDragMove={handleDragMove}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<ProtectionProvider value={projected as any}>
|
||||
<SortableContext
|
||||
items={flattenedItems}
|
||||
// strategy={verticalListSortingStrategy}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<Editable
|
||||
className={css('black mt4 outlineNone')}
|
||||
renderLeaf={(props) => <Leaf {...props} />}
|
||||
renderElement={renderElement}
|
||||
decorate={decorate as any} //
|
||||
onCompositionUpdate={onOnCompositionEvent}
|
||||
onCompositionEnd={onOnCompositionEvent}
|
||||
onKeyDown={keyDown}
|
||||
onDOMBeforeInput={onDOMBeforeInput}
|
||||
onBlur={blur}
|
||||
/>
|
||||
|
||||
{createPortal(
|
||||
<DragOverlay
|
||||
adjustScale={false}
|
||||
dropAnimation={dropAnimationConfig}
|
||||
>
|
||||
{activeId && activeItem ? <DragOverlayPreview /> : null}
|
||||
</DragOverlay>,
|
||||
document.body,
|
||||
)}
|
||||
</SortableContext>
|
||||
</ProtectionProvider>
|
||||
</DndContext>
|
||||
<ClickablePadding />
|
||||
</Slate>
|
||||
)
|
||||
|
||||
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', '')
|
||||
}
|
||||
}
|
||||
|
||||
18
packages/editor/src/components/ProtectionProvider.tsx
Normal file
18
packages/editor/src/components/ProtectionProvider.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export interface ProtectionContext {
|
||||
depth: number
|
||||
maxDepth: number
|
||||
minDepth: number
|
||||
parentId: string
|
||||
}
|
||||
|
||||
export const protectionContext = createContext<ProtectionContext>(
|
||||
{} as ProtectionContext,
|
||||
)
|
||||
|
||||
export const ProtectionProvider = protectionContext.Provider
|
||||
|
||||
export function useProtectionContext() {
|
||||
return useContext(protectionContext)
|
||||
}
|
||||
@@ -8,7 +8,11 @@ type Element = {
|
||||
children: Array<{ text: string }>
|
||||
}
|
||||
|
||||
export const isRootNode = () => {}
|
||||
export type WithFlattenedProps<T> = T & {
|
||||
parentId: string | null // parent node id
|
||||
depth: number
|
||||
index: number
|
||||
}
|
||||
|
||||
export class Node {
|
||||
constructor(public raw: INode) {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> = T & {
|
||||
parentId: string | null // parent node id
|
||||
depth: number
|
||||
index: number
|
||||
}
|
||||
|
||||
export type FindOptions<T = INode> = {
|
||||
where?: Partial<T>
|
||||
limit?: number
|
||||
|
||||
@@ -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('#', ''))
|
||||
|
||||
52
pnpm-lock.yaml
generated
52
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user