mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(rich-markdown-editor): remove the hover block drag handle and + button (#5550)
The hover handle (drag-to-reorder grip + insert button) added surface area and edge cases for marginal value in a file editor. Block reordering is covered by the keyboard shortcut (Mod-Shift-Arrow) and block insertion by the slash menu, so the handle and + are redundant. Removes the component, its styles, the editor wiring, and the now-unused @tiptap/extension-drag-handle-react dependency. The highlight text-shift fix from the same feature branch is unaffected.
This commit is contained in:
-63
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { Editor } from '@tiptap/core'
|
||||
import { NodeSelection } from '@tiptap/pm/state'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createMarkdownContentExtensions } from '../extensions'
|
||||
import { insertBlockBelow, selectBlockAt } from './drag-handle'
|
||||
|
||||
let editor: Editor | null = null
|
||||
|
||||
afterEach(() => {
|
||||
editor?.destroy()
|
||||
editor = null
|
||||
})
|
||||
|
||||
function mount(markdown: string): Editor {
|
||||
const created = new Editor({ extensions: createMarkdownContentExtensions() })
|
||||
created.commands.setContent(markdown, { contentType: 'markdown' })
|
||||
return created
|
||||
}
|
||||
|
||||
/** Position before the first top-level block whose text contains `word`, or -1. */
|
||||
function blockPos(target: Editor, word: string): number {
|
||||
let pos = -1
|
||||
target.state.doc.forEach((node, offset) => {
|
||||
if (pos < 0 && node.textContent.includes(word)) pos = offset
|
||||
})
|
||||
return pos
|
||||
}
|
||||
|
||||
describe('drag-handle block operations', () => {
|
||||
it('inserts a paragraph after the hovered block and opens the slash menu', () => {
|
||||
editor = mount('# One\n\nTwo para')
|
||||
insertBlockBelow(editor, blockPos(editor, 'Two para'))
|
||||
const md = editor.getMarkdown().trim()
|
||||
expect(md).toContain('Two para')
|
||||
expect(md.split('Two para')[1]).toContain('/')
|
||||
})
|
||||
|
||||
it('inserts a sibling after a whole list, not a nested list item', () => {
|
||||
editor = mount('- a\n- b')
|
||||
insertBlockBelow(editor, blockPos(editor, 'a'))
|
||||
expect(editor.getJSON().content?.[0]?.type).toBe('bulletList')
|
||||
expect(editor.getJSON().content?.some((node) => node.type === 'paragraph')).toBe(true)
|
||||
})
|
||||
|
||||
it('selects the block as a NodeSelection', () => {
|
||||
editor = mount('# One\n\nTwo para')
|
||||
selectBlockAt(editor, blockPos(editor, 'Two para'))
|
||||
const { selection } = editor.state
|
||||
expect(selection instanceof NodeSelection).toBe(true)
|
||||
expect((selection as NodeSelection).node.textContent).toBe('Two para')
|
||||
})
|
||||
|
||||
it('is a no-op at an unresolved position', () => {
|
||||
editor = mount('# One')
|
||||
const before = editor.getMarkdown()
|
||||
insertBlockBelow(editor, -1)
|
||||
selectBlockAt(editor, -1)
|
||||
expect(editor.getMarkdown()).toBe(before)
|
||||
})
|
||||
})
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useRef } from 'react'
|
||||
import DragHandle from '@tiptap/extension-drag-handle-react'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { GripVertical, Plus } from 'lucide-react'
|
||||
|
||||
interface BlockDragHandleProps {
|
||||
editor: Editor
|
||||
}
|
||||
|
||||
interface NodeChangeData {
|
||||
pos: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an empty paragraph immediately after the top-level block at `pos` and opens the slash menu
|
||||
* there, so the `+` control adds a new block below the hovered one. A no-op if `pos` doesn't resolve to
|
||||
* a node (e.g. the handle hasn't hovered a block yet).
|
||||
*/
|
||||
export function insertBlockBelow(editor: Editor, pos: number): void {
|
||||
const node = pos >= 0 ? editor.state.doc.nodeAt(pos) : null
|
||||
if (!node) return
|
||||
const insertAt = pos + node.nodeSize
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(insertAt, { type: 'paragraph' })
|
||||
.setTextSelection(insertAt + 1)
|
||||
.insertContent('/')
|
||||
.run()
|
||||
}
|
||||
|
||||
/** Selects the top-level block at `pos` as a NodeSelection (the grip's click affordance). */
|
||||
export function selectBlockAt(editor: Editor, pos: number): void {
|
||||
if (pos < 0) return
|
||||
editor.chain().setNodeSelection(pos).run()
|
||||
editor.view.focus()
|
||||
}
|
||||
|
||||
/**
|
||||
* Left-margin block controls revealed on block hover: a `+` that inserts a paragraph below the hovered
|
||||
* block and opens the slash menu ({@link insertBlockBelow}), and a `⠿` grip that drags to reorder (via
|
||||
* `@tiptap/extension-drag-handle`) or, on a plain click, selects the block ({@link selectBlockAt}). The
|
||||
* keyboard equivalent of the reorder is `Mod-Shift-Arrow` (see the block-mover extension).
|
||||
*/
|
||||
export function BlockDragHandle({ editor }: BlockDragHandleProps) {
|
||||
const hoveredPosRef = useRef(-1)
|
||||
|
||||
const handleNodeChange = useCallback((data: NodeChangeData) => {
|
||||
hoveredPosRef.current = data.pos
|
||||
}, [])
|
||||
|
||||
const insertBelow = useCallback(() => {
|
||||
insertBlockBelow(editor, hoveredPosRef.current)
|
||||
}, [editor])
|
||||
|
||||
const selectBlock = useCallback(() => {
|
||||
selectBlockAt(editor, hoveredPosRef.current)
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<DragHandle editor={editor} onNodeChange={handleNodeChange}>
|
||||
<div className='rich-md-block-controls'>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Insert block below'
|
||||
className='rich-md-block-btn'
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onClick={insertBelow}
|
||||
>
|
||||
<Plus size={15} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Drag to reorder, or click to select the block'
|
||||
className='rich-md-block-btn rich-md-block-grip'
|
||||
onClick={selectBlock}
|
||||
>
|
||||
<GripVertical size={15} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</DragHandle>
|
||||
)
|
||||
}
|
||||
-45
@@ -414,51 +414,6 @@
|
||||
-webkit-box-decoration-break: clone;
|
||||
}
|
||||
|
||||
/*
|
||||
* Left-margin block controls (the `+` / `⠿` handle revealed on block hover). `@tiptap/extension-drag-
|
||||
* handle` positions the wrapper; these rules style the two buttons — a subtle icon pair that darkens on
|
||||
* hover, with the grip showing a grab cursor and the add button a pointer.
|
||||
*/
|
||||
.rich-md-block-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.rich-md-block-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
color: var(--text-subtle);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.12s ease,
|
||||
color 0.12s ease;
|
||||
}
|
||||
|
||||
.rich-md-block-btn:hover,
|
||||
.rich-md-block-btn:focus-visible {
|
||||
background-color: var(--surface-active);
|
||||
color: var(--text-icon);
|
||||
}
|
||||
|
||||
.rich-md-block-grip {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.rich-md-block-grip:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/*
|
||||
* Field variant (modal embed): match the surrounding chip fields' typography exactly —
|
||||
* body at the chip `text-sm` (14px) scale and the placeholder at `--text-muted` (not the
|
||||
|
||||
-2
@@ -23,7 +23,6 @@ import {
|
||||
import { parseMarkdownToDoc } from './markdown-parse'
|
||||
import { useEditorMentions } from './mention'
|
||||
import { EditorBubbleMenu } from './menus/bubble-menu'
|
||||
import { BlockDragHandle } from './menus/drag-handle'
|
||||
import { LinkHoverCard } from './menus/link-hover-card'
|
||||
import { TableBubbleMenu } from './menus/table-menu'
|
||||
import { normalizeMarkdownContent } from './normalize-content'
|
||||
@@ -447,7 +446,6 @@ export function LoadedRichMarkdownEditor({
|
||||
{editor && <EditorBubbleMenu editor={editor} scrollContainerRef={containerRef} />}
|
||||
{editor && <TableBubbleMenu editor={editor} scrollContainerRef={containerRef} />}
|
||||
{editor && <LinkHoverCard editor={editor} />}
|
||||
{editor && isEditable && <BlockDragHandle editor={editor} />}
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type='file'
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
"@tanstack/react-virtual": "3.13.24",
|
||||
"@tiptap/core": "3.26.1",
|
||||
"@tiptap/extension-code-block": "3.26.1",
|
||||
"@tiptap/extension-drag-handle-react": "3.26.1",
|
||||
"@tiptap/extension-image": "3.26.1",
|
||||
"@tiptap/extension-list": "3.26.1",
|
||||
"@tiptap/extension-placeholder": "3.26.1",
|
||||
|
||||
@@ -183,7 +183,6 @@
|
||||
"@tanstack/react-virtual": "3.13.24",
|
||||
"@tiptap/core": "3.26.1",
|
||||
"@tiptap/extension-code-block": "3.26.1",
|
||||
"@tiptap/extension-drag-handle-react": "3.26.1",
|
||||
"@tiptap/extension-image": "3.26.1",
|
||||
"@tiptap/extension-list": "3.26.1",
|
||||
"@tiptap/extension-placeholder": "3.26.1",
|
||||
@@ -1751,14 +1750,8 @@
|
||||
|
||||
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1", "@tiptap/pm": "3.26.1" } }, "sha512-NY7SYqcrqDVYTSWyaNGdSfCims6pOHoRQ2Rh4DEFb/rb8gLVkqbLZhcHzQCVfinlPqgV3xWF6cYMORwmnlBkXQ=="],
|
||||
|
||||
"@tiptap/extension-collaboration": ["@tiptap/extension-collaboration@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1", "@tiptap/pm": "3.26.1", "@tiptap/y-tiptap": "^3.0.5", "yjs": "^13" } }, "sha512-NLF3tWPla1bg9VAaICTrheEjDi9ZFGk1HhoALfVHWSwIqnUpVxSubyBxw/PFWyHYZNQrxaGvypf457NHHINolA=="],
|
||||
|
||||
"@tiptap/extension-document": ["@tiptap/extension-document@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1" } }, "sha512-6W2vZjvi0Mv+4xEtwMDGhWwo7FotWR6eKfmntmduvehWevFpMxOKcTtyotjLigfZv738y50YWmvbaPuAPJG3BA=="],
|
||||
|
||||
"@tiptap/extension-drag-handle": ["@tiptap/extension-drag-handle@3.26.1", "", { "dependencies": { "@floating-ui/dom": "^1.6.13" }, "peerDependencies": { "@tiptap/core": "3.26.1", "@tiptap/extension-collaboration": "3.26.1", "@tiptap/extension-node-range": "3.26.1", "@tiptap/pm": "3.26.1", "@tiptap/y-tiptap": "^3.0.5" } }, "sha512-LaZDIBjBT1b7vJImwe6GdNCQlTKeIc0bbF+GZGAVzuLvjczBmWaZRaOleHELomzPeAY/t9cyV8xNJuq8pN+e3A=="],
|
||||
|
||||
"@tiptap/extension-drag-handle-react": ["@tiptap/extension-drag-handle-react@3.26.1", "", { "peerDependencies": { "@tiptap/extension-drag-handle": "3.26.1", "@tiptap/pm": "3.26.1", "@tiptap/react": "3.26.1", "react": "^16.8 || ^17 || ^18 || ^19", "react-dom": "^16.8 || ^17 || ^18 || ^19" } }, "sha512-70euHOq3aeeVJAksjNeX2yUoWGPiOwgnp8o12Eu9B/wbRw7LHQaazEEUeRE/ed3azEYKaBRfBS8CWv9N4hF1Zw=="],
|
||||
|
||||
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.26.1", "", { "peerDependencies": { "@tiptap/extensions": "3.26.1" } }, "sha512-eVq3BvFIa3YD+pBIlj1i72vYEixlegGVKHnSYiVF2ovkQOSAH9sca7pkq6WgV1sMTCyWCU8e+WznTqtydvHUWA=="],
|
||||
|
||||
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.26.1", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.26.1", "@tiptap/pm": "3.26.1" } }, "sha512-xn0g4m/q2bjG+hULPwp6Aqb/6wpzUtc65jOhgJsG/S3Ey3kLJGUvZBuhozwNFu8FcugxM1fMUpNhkJkodCCGFw=="],
|
||||
@@ -1783,8 +1776,6 @@
|
||||
|
||||
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.26.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.1" } }, "sha512-EReSayePO6SIxtRbxx+7KfBQreWHvoZmMb3O/RemfT8W6J0hCG5N/Rh8Z12+YZOnCDRXJ4RzFpAikYka3E54jQ=="],
|
||||
|
||||
"@tiptap/extension-node-range": ["@tiptap/extension-node-range@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1", "@tiptap/pm": "3.26.1" } }, "sha512-tsxy2ROyK3LLUeV/rVbDyXEZNCVwirul7Aj0ykHWRch9c+4j+bJkvdLwOhaBE/eGCooniwh1DkpYjvzzoC8owQ=="],
|
||||
|
||||
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.26.1", "", { "peerDependencies": { "@tiptap/extension-list": "3.26.1" } }, "sha512-LeFPeFwb7ylkQVuuaHj+niu7WhWHpjDOi1GKZJE/ohOa2lgt7P221HMqhUzPiDlXOExN72oWTNmXUlT0ymCTkw=="],
|
||||
|
||||
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1" } }, "sha512-OkBeYUNM3eTzjm3z6IcC3NHryOX8g3eGNI86P/B+tFoFQSRuzLsKZU50ARCfIiLLg812NjcqujeJ1eX3BKDZrw=="],
|
||||
@@ -1811,8 +1802,6 @@
|
||||
|
||||
"@tiptap/suggestion": ["@tiptap/suggestion@3.26.1", "", { "peerDependencies": { "@tiptap/core": "3.26.1", "@tiptap/pm": "3.26.1" } }, "sha512-Bg8IyuDC92InSPzcHvCT3+ZDCJSMJIEINdFg513RPQzwZTw1dsrU0K00XYcDT6lOhZwLM2IVTiE6sZl2GY25Rg=="],
|
||||
|
||||
"@tiptap/y-tiptap": ["@tiptap/y-tiptap@3.0.6", "", { "dependencies": { "lib0": "^0.2.100" }, "peerDependencies": { "prosemirror-model": "^1.7.1", "prosemirror-state": "^1.2.3", "prosemirror-view": "^1.9.10", "y-protocols": "^1.0.1", "yjs": "^13.5.38" } }, "sha512-kcGeVGKtq/cPGVseNKjtmtcY2WXUAEm1SqS5x0Smubj4nOCRyPiHg6kY4QuuZhmXjTK7hdo8chokkPUKWXPE9Q=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@trigger.dev/build": ["@trigger.dev/build@4.4.3", "", { "dependencies": { "@prisma/config": "^6.10.0", "@trigger.dev/core": "4.4.3", "mlly": "^1.7.1", "pkg-types": "^1.1.3", "resolve": "^1.22.8", "tinyglobby": "^0.2.2", "tsconfck": "3.1.3" } }, "sha512-t/hYmQiv2SdrUao9scoczrvfhyzSLkuT8DNyiBt9q29GKct37zytWyAo16hpN2Uf+yXh0EkdnkHbfR9odF0YtQ=="],
|
||||
@@ -2929,8 +2918,6 @@
|
||||
|
||||
"isomorphic-unfetch": ["isomorphic-unfetch@3.1.0", "", { "dependencies": { "node-fetch": "^2.6.1", "unfetch": "^4.2.0" } }, "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q=="],
|
||||
|
||||
"isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
|
||||
|
||||
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||
|
||||
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
|
||||
@@ -2993,8 +2980,6 @@
|
||||
|
||||
"leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="],
|
||||
|
||||
"lib0": ["lib0@0.2.117", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js", "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js" } }, "sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw=="],
|
||||
|
||||
"libbase64": ["libbase64@1.3.0", "", {}, "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg=="],
|
||||
|
||||
"libmime": ["libmime@5.3.7", "", { "dependencies": { "encoding-japanese": "2.2.0", "iconv-lite": "0.6.3", "libbase64": "1.3.0", "libqp": "2.1.1" } }, "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw=="],
|
||||
@@ -4107,8 +4092,6 @@
|
||||
|
||||
"xpath": ["xpath@0.0.34", "", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="],
|
||||
|
||||
"y-protocols": ["y-protocols@1.0.7", "", { "dependencies": { "lib0": "^0.2.85" }, "peerDependencies": { "yjs": "^13.0.0" } }, "sha512-YSVsLoXxO67J6eE/nV4AtFtT3QEotZf5sK5BHxFBXso7VDUT3Tx07IfA6hsu5Q5OmBdMkQVmFZ9QOA7fikWvnw=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
@@ -4121,8 +4104,6 @@
|
||||
|
||||
"yauzl": ["yauzl@3.4.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw=="],
|
||||
|
||||
"yjs": ["yjs@13.6.31", "", { "dependencies": { "lib0": "^0.2.99" } }, "sha512-Eq+5BRfbeGyqGVrTJL3bEcr8gKkxPuyuoHmAwpk52fDb8kOVMrfVSTRPd6yiGgX5Fskb96qCRjzjbRjrL4YEnw=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"yoga-wasm-web": ["yoga-wasm-web@0.3.3", "", {}, "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="],
|
||||
|
||||
Reference in New Issue
Block a user