Commit 8752466c authored by Яков's avatar Яков
Browse files

update fix issue

parent 2bf763b4
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.1.77", "version": "1.1.78",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -746,11 +746,6 @@ const QEditor = ({ ...@@ -746,11 +746,6 @@ const QEditor = ({
const { state, view } = editor const { state, view } = editor
const { $from } = state.selection const { $from } = state.selection
// Всегда вставляем после текущего блока верхнего уровня,
// а не в позицию курсора — иначе insertContent может «съесть»
// пустой параграф, разделяющий два раскрывающихся списка.
const insertPos = $from.after(Math.min($from.depth, 1))
const schema = state.schema const schema = state.schema
const toggleNode = schema.nodes.toggleBlock.create( const toggleNode = schema.nodes.toggleBlock.create(
{ title: 'Заголовок', open: true }, { title: 'Заголовок', open: true },
...@@ -761,7 +756,18 @@ const QEditor = ({ ...@@ -761,7 +756,18 @@ const QEditor = ({
) )
const paraNode = schema.nodes.paragraph.create() const paraNode = schema.nodes.paragraph.create()
view.dispatch(state.tr.insert(insertPos, [toggleNode, paraNode])) const depth = Math.min($from.depth, 1)
const currentNode = $from.node(depth)
// Если текущий блок — пустой параграф, заменяем его
if (currentNode.type.name === 'paragraph' && currentNode.childCount === 0) {
const from = $from.before(depth)
const to = $from.after(depth)
view.dispatch(state.tr.replaceWith(from, to, [toggleNode, paraNode]))
} else {
const insertPos = $from.after(depth)
view.dispatch(state.tr.insert(insertPos, [toggleNode, paraNode]))
}
}, },
}, },
......
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { TextSelection, NodeSelection, Plugin, PluginKey } from 'prosemirror-state' import { TextSelection, Plugin, PluginKey } from 'prosemirror-state'
const DragHandleIcon = () => ( const DragHandleIcon = () => (
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg">
...@@ -15,7 +15,7 @@ const DragHandleIcon = () => ( ...@@ -15,7 +15,7 @@ const DragHandleIcon = () => (
) )
// React компонент NodeView // React компонент NodeView
export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, selected}) => { export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => {
const open = node.attrs.open const open = node.attrs.open
const title = node.attrs.title const title = node.attrs.title
...@@ -33,65 +33,135 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, se ...@@ -33,65 +33,135 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, se
} }
}, [title]) }, [title])
const handleDragPointerDown = (e) => {
e.preventDefault()
const nodePos = getPos?.()
if (typeof nodePos !== 'number') return
const dragNode = editor.state.doc.nodeAt(nodePos)
if (!dragNode || dragNode.type.name !== 'toggleBlock') return
const editorDom = editor.view.dom
// Индикатор места вставки
const indicator = document.createElement('div')
indicator.style.cssText = 'position:fixed;left:0;right:0;height:2px;background:#1677ff;z-index:9999;pointer-events:none;display:none;border-radius:1px;'
document.body.appendChild(indicator)
document.body.style.userSelect = 'none'
document.body.classList.add('toggle-dragging')
let currentTargetPos = null
const getBlockInfos = () => {
const blocks = []
editor.state.doc.forEach((blockNode, offset) => {
try {
const domResult = editor.view.domAtPos(offset + 1)
let el = domResult.node instanceof Element ? domResult.node : domResult.node.parentElement
while (el && el.parentElement !== editorDom) {
el = el.parentElement
}
if (el) {
blocks.push({ offset, nodeSize: blockNode.nodeSize, rect: el.getBoundingClientRect() })
}
} catch {}
})
return blocks
}
const onPointerMove = (moveEvent) => {
const mouseY = moveEvent.clientY
const blocks = getBlockInfos()
let bestY = null
let bestPos = null
let bestDist = Infinity
for (const block of blocks) {
const { rect, offset, nodeSize } = block
const midY = (rect.top + rect.bottom) / 2
if (mouseY <= midY) {
const dist = Math.abs(mouseY - rect.top)
if (dist < bestDist) {
bestDist = dist
bestY = rect.top
bestPos = offset
}
} else {
const dist = Math.abs(mouseY - rect.bottom)
if (dist < bestDist) {
bestDist = dist
bestY = rect.bottom
bestPos = offset + nodeSize
}
}
}
if (bestPos !== null) {
const isNoOp = bestPos === nodePos || bestPos === nodePos + dragNode.nodeSize
const isOnSelf = bestPos > nodePos && bestPos < nodePos + dragNode.nodeSize
if (isNoOp || isOnSelf) {
indicator.style.display = 'none'
currentTargetPos = null
} else {
indicator.style.display = 'block'
indicator.style.top = bestY + 'px'
currentTargetPos = bestPos
}
}
}
const cleanup = () => {
if (document.body.contains(indicator)) document.body.removeChild(indicator)
document.body.style.userSelect = ''
document.body.classList.remove('toggle-dragging')
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
}
const onPointerUp = () => {
const targetPos = currentTargetPos
cleanup()
if (targetPos === null) return
const currentState = editor.view.state
const movingNode = currentState.doc.nodeAt(nodePos)
if (!movingNode || movingNode.type.name !== 'toggleBlock') return
const from = nodePos
const to = nodePos + movingNode.nodeSize
const tr = currentState.tr
if (targetPos <= from) {
tr.insert(targetPos, movingNode)
tr.delete(from + movingNode.nodeSize, to + movingNode.nodeSize)
} else {
tr.delete(from, to)
tr.insert(targetPos - movingNode.nodeSize, movingNode)
}
if (tr.steps.length > 0) {
editor.view.dispatch(tr)
}
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
}
return ( return (
<NodeViewWrapper className="toggle-block" data-open={open}> <NodeViewWrapper className="toggle-block" data-open={open}>
{selected && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}
} catch (err) {
console.warn('Ошибка удаления toggleBlock:', err)
}
}}
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 10,
background: 'white',
border: '1px solid #d9d9d9',
borderRadius: '50%',
width: 20,
height: 20,
fontSize: 12,
lineHeight: 1,
cursor: 'pointer',
padding: '0px 0px 2px 0px',
color: '#ff4d4f'
}}
>
×
</button>
)}
<div className="toggle-block-inner"> <div className="toggle-block-inner">
<div className="toggle-header-wrapper"> <div className="toggle-header-wrapper">
<div <div
className="toggle-drag-handle" className="toggle-drag-handle"
contentEditable={false} contentEditable={false}
draggable={true}
data-drag-handle=""
title="Перетащить" title="Перетащить"
onDragStart={(e) => { onPointerDown={handleDragPointerDown}
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.setSelection(
NodeSelection.create(editor.view.state.doc, pos)
)
)
}
} catch {}
}}
> >
<DragHandleIcon /> <DragHandleIcon />
</div> </div>
...@@ -116,6 +186,25 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, se ...@@ -116,6 +186,25 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, se
} }
}} }}
/> />
<button
type="button"
className="toggle-block-delete"
onClick={(e) => {
e.stopPropagation()
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}
} catch (err) {
console.warn('Ошибка удаления toggleBlock:', err)
}
}}
>
×
</button>
</div> </div>
</div> </div>
<div <div
...@@ -169,7 +258,7 @@ const ToggleBlock = Node.create({ ...@@ -169,7 +258,7 @@ const ToggleBlock = Node.create({
name: 'toggleBlock', name: 'toggleBlock',
group: 'block', group: 'block',
content: 'block+', content: 'block+',
draggable: true, draggable: false,
addAttributes () { addAttributes () {
return { return {
...@@ -216,6 +305,74 @@ const ToggleBlock = Node.create({ ...@@ -216,6 +305,74 @@ const ToggleBlock = Node.create({
return ReactNodeViewRenderer(ToggleBlockComponent) return ReactNodeViewRenderer(ToggleBlockComponent)
}, },
addKeyboardShortcuts () {
return {
Backspace: ({ editor }) => {
const { state } = editor
const { $from, empty } = state.selection
if (!empty || $from.parentOffset !== 0) return false
// Случай А: курсор в начале блока, предыдущий сосед — toggleBlock
// Если блок пустой — удаляем его; в любом случае запрещаем слияние с toggleBlock
if ($from.depth === 1) {
const blockPos = $from.before(1)
const docIndex = $from.index(0)
if (docIndex > 0) {
const prevSibling = state.doc.child(docIndex - 1)
if (prevSibling.type.name === 'toggleBlock') {
if ($from.parent.childCount === 0) {
editor.view.dispatch(
state.tr.delete(blockPos, blockPos + $from.parent.nodeSize)
)
}
return true
}
}
}
// Случай Б: курсор в самом начале первого дочернего элемента toggleBlock
// → запрещаем слияние toggleBlock с предыдущим блоком
for (let d = $from.depth; d >= 1; d--) {
if ($from.node(d).type.name === 'toggleBlock') {
const togglePos = $from.before(d)
if ($from.pos <= togglePos + 2) {
return true
}
break
}
}
return false
},
Delete: ({ editor }) => {
const { state } = editor
const { $from, empty } = state.selection
if (!empty) return false
// Курсор в конце блока, следующий сосед — toggleBlock
// → запрещаем слияние (Delete на пустом параграфе перед toggleBlock)
if ($from.depth === 1 && $from.parentOffset === $from.parent.content.size) {
const docIndex = $from.index(0)
if (docIndex < state.doc.childCount - 1) {
const nextSibling = state.doc.child(docIndex + 1)
if (nextSibling.type.name === 'toggleBlock') {
if ($from.parent.childCount === 0) {
const blockPos = $from.before(1)
editor.view.dispatch(
state.tr.delete(blockPos, blockPos + $from.parent.nodeSize)
)
}
return true
}
}
}
return false
},
}
},
addProseMirrorPlugins () { addProseMirrorPlugins () {
// Миграция: убираем пустые параграфы в начале toggle-блоков (артефакт старого бага) // Миграция: убираем пустые параграфы в начале toggle-блоков (артефакт старого бага)
const cleanLeadingEmptyParagraphs = (state) => { const cleanLeadingEmptyParagraphs = (state) => {
......
...@@ -1639,6 +1639,30 @@ body{ ...@@ -1639,6 +1639,30 @@ body{
} }
.toggle-block { .toggle-block {
margin-bottom: 12px; margin-bottom: 12px;
&:hover .toggle-block-delete {
opacity: 1;
}
}
.toggle-block-delete {
flex-shrink: 0;
margin-left: 8px;
background: white;
border: 1px solid #d9d9d9;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0;
color: #ff4d4f;
opacity: 0;
transition: opacity 0.15s;
display: flex;
align-items: center;
justify-content: center;
} }
.toggle-header-wrapper { .toggle-header-wrapper {
position: relative; position: relative;
...@@ -1664,6 +1688,10 @@ body{ ...@@ -1664,6 +1688,10 @@ body{
cursor: grabbing; cursor: grabbing;
} }
body.toggle-dragging & {
cursor: grabbing;
}
&:hover { &:hover {
color: #888; color: #888;
background: rgba(0,0,0,0.05); background: rgba(0,0,0,0.05);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment