feat: can dnd in List

This commit is contained in:
0xzion
2023-11-12 10:04:10 +08:00
parent 7c8c08acc1
commit cd0e2086de
37 changed files with 511 additions and 234 deletions

View File

@@ -54,6 +54,7 @@ const config = {
'@penx/table',
'@penx/database',
'@penx/tag',
'@penx/dnd-projection',
'@penx/block-selector',
'@penx/editor-leaf',
'@penx/trpc-client',

View File

@@ -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",

View 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
}

View File

@@ -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'

View File

@@ -1,4 +1,4 @@
import { withLists } from 'slate-lists'
import { listSchema } from './listSchema'
import { listSchema } from '../listSchema'
export const withListsPlugin = withLists(listSchema)

View File

@@ -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

View 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 />
)
}

View File

@@ -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

View File

@@ -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>
)

View File

@@ -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>

View File

@@ -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:*",

View File

@@ -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}>

View File

@@ -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>

View File

@@ -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)

View File

@@ -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
)
},
)

View File

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

View File

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

View File

@@ -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" />
}

View File

@@ -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" />
}

View File

@@ -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"

View File

@@ -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,

View File

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

View 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:*"
}
}

View File

@@ -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

View File

@@ -0,0 +1 @@
export * from './getProjection'

View File

@@ -0,0 +1,8 @@
{
"extends": "tsconfig/react-library.json",
"compilerOptions": {
"lib": ["ESNext", "DOM"]
},
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -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() {

View File

@@ -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:*",

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

View File

@@ -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', '')
}
}

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

View File

@@ -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) {}

View File

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

View File

@@ -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

View File

@@ -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
View File

@@ -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