Commit 66da5bdf authored by Яков's avatar Яков
Browse files

update fix issue

parent 5d537563
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.1.75", "version": "1.1.76",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -3,8 +3,19 @@ import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap ...@@ -3,8 +3,19 @@ import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } from '@tiptap
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { TextSelection, Plugin, PluginKey } from 'prosemirror-state' import { TextSelection, Plugin, PluginKey } from 'prosemirror-state'
const DragHandleIcon = () => (
<svg width="10" height="16" viewBox="0 0 10 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="3" cy="3" r="1.5" fill="currentColor"/>
<circle cx="7" cy="3" r="1.5" fill="currentColor"/>
<circle cx="3" cy="8" r="1.5" fill="currentColor"/>
<circle cx="7" cy="8" r="1.5" fill="currentColor"/>
<circle cx="3" cy="13" r="1.5" fill="currentColor"/>
<circle cx="7" cy="13" r="1.5" fill="currentColor"/>
</svg>
)
// React компонент NodeView // React компонент NodeView
export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) => { export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor, selected}) => {
const open = node.attrs.open const open = node.attrs.open
const title = node.attrs.title const title = node.attrs.title
...@@ -17,57 +28,67 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) = ...@@ -17,57 +28,67 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
useEffect(() => { useEffect(() => {
if (measurerRef.current) { if (measurerRef.current) {
const width = measurerRef.current.offsetWidth + 100 // небольшой запас const width = measurerRef.current.offsetWidth + 100
setInputWidth(`${width}px`) setInputWidth(`${width}px`)
} }
}, [title]) }, [title])
return ( return (
<NodeViewWrapper className="toggle-block" data-open={open}> <NodeViewWrapper className="toggle-block" data-open={open}>
<button {selected && (
type="button" <button
onClick={(e) => { type="button"
e.stopPropagation() onClick={(e) => {
try { e.stopPropagation()
const pos = getPos?.() try {
if (typeof pos === 'number') { const pos = getPos?.()
editor.view.dispatch( if (typeof pos === 'number') {
editor.view.state.tr.delete(pos, pos + node.nodeSize) editor.view.dispatch(
) editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
}
} catch (err) {
console.warn('Ошибка удаления toggleBlock:', err)
} }
} catch (err) { }}
console.warn('Ошибка удаления toggleBlock:', err) style={{
} position: 'absolute',
}} top: 4,
style={{ right: 4,
position: 'absolute', zIndex: 10,
top: 4, background: 'white',
right: 4, border: '1px solid #d9d9d9',
zIndex: 10, borderRadius: '50%',
background: 'white', width: 20,
border: '1px solid #ccc', height: 20,
borderRadius: '50%', fontSize: 12,
width: 20, lineHeight: 1,
height: 20, cursor: 'pointer',
fontSize: 12, padding: '0px 0px 2px 0px',
lineHeight: 1, color: '#ff4d4f'
cursor: 'pointer', }}
padding: '0px 0px 2px 0px', >
color: '#ff4d4f' ×
}} </button>
> )}
×
</button>
<div className="toggle-block-inner"> <div className="toggle-block-inner">
<div className="toggle-header-wrapper"> <div className="toggle-header-wrapper">
<span <div
className="toggle-header-measurer" className="toggle-drag-handle"
ref={measurerRef} contentEditable={false}
aria-hidden="true" data-drag-handle=""
> title="Перетащить"
>
<DragHandleIcon />
</div>
<span
className="toggle-header-measurer"
ref={measurerRef}
aria-hidden="true"
>
{title || 'Заголовок'} {title || 'Заголовок'}
</span> </span>
<button className={"toggle-button " + (open ? 'open' : '')} onClick={toggle}></button> <button className={"toggle-button " + (open ? 'open' : '')} onClick={toggle}></button>
<input <input
type="text" type="text"
...@@ -78,12 +99,10 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) = ...@@ -78,12 +99,10 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
style={{ width: inputWidth }} style={{ width: inputWidth }}
onFocus={(e) => { onFocus={(e) => {
if (title.trim() === 'Заголовок') { if (title.trim() === 'Заголовок') {
// выделить весь текст
setTimeout(() => e.target.select(), 0) setTimeout(() => e.target.select(), 0)
} }
}} }}
/> />
</div> </div>
</div> </div>
<div <div
...@@ -111,7 +130,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) = ...@@ -111,7 +130,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
firstBlock.type.name === 'paragraph' && firstBlock.type.name === 'paragraph' &&
firstBlock.textContent.trim() === 'Введите подробности...' firstBlock.textContent.trim() === 'Введите подробности...'
) { ) {
const from = pos + 2 // +1 = paragraph, +1 = start of text inside it const from = pos + 2
const to = from + firstBlock.textContent.length const to = from + firstBlock.textContent.length
const tr = editor.state.tr.setSelection( const tr = editor.state.tr.setSelection(
...@@ -137,6 +156,7 @@ const ToggleBlock = Node.create({ ...@@ -137,6 +156,7 @@ const ToggleBlock = Node.create({
name: 'toggleBlock', name: 'toggleBlock',
group: 'block', group: 'block',
content: 'block+', content: 'block+',
draggable: true,
addAttributes () { addAttributes () {
return { return {
...@@ -153,7 +173,6 @@ const ToggleBlock = Node.create({ ...@@ -153,7 +173,6 @@ const ToggleBlock = Node.create({
const titleEl = wrapper?.querySelector('.toggle-header') const titleEl = wrapper?.querySelector('.toggle-header')
const title = titleEl?.textContent?.trim() || 'Заголовок' const title = titleEl?.textContent?.trim() || 'Заголовок'
// удаляем заголовок из DOM, чтобы не попал в content
if (titleEl?.parentNode) { if (titleEl?.parentNode) {
titleEl.parentNode.removeChild(titleEl) titleEl.parentNode.removeChild(titleEl)
} }
...@@ -166,9 +185,7 @@ const ToggleBlock = Node.create({ ...@@ -166,9 +185,7 @@ const ToggleBlock = Node.create({
renderHTML ({HTMLAttributes}) { renderHTML ({HTMLAttributes}) {
return [ return [
'div', 'div',
{ {class: 'toggle-block'},
class: 'toggle-block',
},
[ [
'div', 'div',
{class: 'toggle-block-inner'}, {class: 'toggle-block-inner'},
...@@ -184,32 +201,7 @@ const ToggleBlock = Node.create({ ...@@ -184,32 +201,7 @@ const ToggleBlock = Node.create({
}, },
addProseMirrorPlugins () { addProseMirrorPlugins () {
const buildGapFix = (state) => { // Миграция: убираем пустые параграфы в начале toggle-блоков (артефакт старого бага)
const doc = state.doc
const tr = state.tr
let additionalOffset = 0
let pos = 0
for (let i = 0; i < doc.childCount - 1; i++) {
const node = doc.child(i)
const nextNode = doc.child(i + 1)
pos += node.nodeSize
if (
node.type.name === 'toggleBlock' &&
nextNode.type.name === 'toggleBlock'
) {
tr.insert(
pos + additionalOffset,
state.schema.nodes.paragraph.create()
)
additionalOffset += 2
}
}
return additionalOffset > 0 ? tr : null
}
const cleanLeadingEmptyParagraphs = (state) => { const cleanLeadingEmptyParagraphs = (state) => {
const doc = state.doc const doc = state.doc
const tr = state.tr const tr = state.tr
...@@ -218,7 +210,6 @@ const ToggleBlock = Node.create({ ...@@ -218,7 +210,6 @@ const ToggleBlock = Node.create({
doc.forEach((node, pos) => { doc.forEach((node, pos) => {
if (node.type.name !== 'toggleBlock') return if (node.type.name !== 'toggleBlock') return
// Считаем сколько пустых параграфов стоят первыми подряд
let emptyCount = 0 let emptyCount = 0
for (let i = 0; i < node.childCount; i++) { for (let i = 0; i < node.childCount; i++) {
const child = node.child(i) const child = node.child(i)
...@@ -229,12 +220,10 @@ const ToggleBlock = Node.create({ ...@@ -229,12 +220,10 @@ const ToggleBlock = Node.create({
} }
} }
// Оставляем хотя бы один блок — удаляем только если после пустых есть что-то ещё
const removable = node.childCount > emptyCount ? emptyCount : Math.max(0, emptyCount - 1) const removable = node.childCount > emptyCount ? emptyCount : Math.max(0, emptyCount - 1)
if (removable === 0) return if (removable === 0) return
// Удаляем removable пустых параграфов с начала содержимого toggle-блока const contentStart = pos + offset + 1
const contentStart = pos + offset + 1 // +1 = открывающий токен toggle-блока
let deleteEnd = contentStart let deleteEnd = contentStart
for (let i = 0; i < removable; i++) { for (let i = 0; i < removable; i++) {
deleteEnd += node.child(i).nodeSize deleteEnd += node.child(i).nodeSize
...@@ -248,21 +237,12 @@ const ToggleBlock = Node.create({ ...@@ -248,21 +237,12 @@ const ToggleBlock = Node.create({
return [ return [
new Plugin({ new Plugin({
key: new PluginKey('toggleBlockGap'), key: new PluginKey('toggleBlockCleanup'),
view (editorView) { view (editorView) {
// Миграция: убираем пустые параграфы в начале toggle-блоков (артефакт старого бага) const tr = cleanLeadingEmptyParagraphs(editorView.state)
const cleanTr = cleanLeadingEmptyParagraphs(editorView.state)
if (cleanTr) editorView.dispatch(cleanTr)
// Fix adjacent toggle blocks present in initially loaded content
const tr = buildGapFix(editorView.state)
if (tr) editorView.dispatch(tr) if (tr) editorView.dispatch(tr)
return {} return {}
}, },
appendTransaction (transactions, _oldState, newState) {
if (!transactions.some(tr => tr.docChanged)) return null
return buildGapFix(newState)
}
}), }),
new Plugin({ new Plugin({
key: new PluginKey('toggleBlockPaste'), key: new PluginKey('toggleBlockPaste'),
...@@ -271,7 +251,6 @@ const ToggleBlock = Node.create({ ...@@ -271,7 +251,6 @@ const ToggleBlock = Node.create({
const { selection } = view.state const { selection } = view.state
const $from = selection.$from const $from = selection.$from
// Ищем toggleBlock в стеке предков
let toggleDepth = -1 let toggleDepth = -1
for (let d = $from.depth; d > 0; d--) { for (let d = $from.depth; d > 0; d--) {
if ($from.node(d).type.name === 'toggleBlock') { if ($from.node(d).type.name === 'toggleBlock') {
...@@ -281,15 +260,12 @@ const ToggleBlock = Node.create({ ...@@ -281,15 +260,12 @@ const ToggleBlock = Node.create({
} }
if (toggleDepth === -1) return false if (toggleDepth === -1) return false
// Только если есть заголовки — иначе стандартная обработка
let hasHeadings = false let hasHeadings = false
slice.content.forEach(node => { slice.content.forEach(node => {
if (node.type.name === 'heading') hasHeadings = true if (node.type.name === 'heading') hasHeadings = true
}) })
if (!hasHeadings) return false if (!hasHeadings) return false
// replaceSelection с openStart > 0 уходит выше toggleBlock и заменяет его.
// Вставляем узлы через tr.insert напрямую — позиция строго внутри toggleBlock.
let insertPos = $from.after(toggleDepth + 1) let insertPos = $from.after(toggleDepth + 1)
const tr = view.state.tr const tr = view.state.tr
...@@ -300,7 +276,6 @@ const ToggleBlock = Node.create({ ...@@ -300,7 +276,6 @@ const ToggleBlock = Node.create({
const nodes = [] const nodes = []
slice.content.forEach(n => nodes.push(n)) slice.content.forEach(n => nodes.push(n))
// Вставляем в обратном порядке в одну позицию — итоговый порядок правильный
for (let i = nodes.length - 1; i >= 0; i--) { for (let i = nodes.length - 1; i >= 0; i--) {
tr.insert(insertPos, nodes[i]) tr.insert(insertPos, nodes[i])
} }
......
...@@ -1642,10 +1642,34 @@ body{ ...@@ -1642,10 +1642,34 @@ body{
} }
.toggle-header-wrapper { .toggle-header-wrapper {
position: relative; position: relative;
display: inline-block; display: inline-flex;
align-items: center;
margin-bottom: 10px; margin-bottom: 10px;
} }
.toggle-drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 24px;
margin-right: 4px;
color: #bbb;
cursor: grab;
flex-shrink: 0;
border-radius: 3px;
transition: color 0.15s;
&:active {
cursor: grabbing;
}
&:hover {
color: #888;
background: rgba(0,0,0,0.05);
}
}
.toggle-header-measurer { .toggle-header-measurer {
visibility: hidden; visibility: hidden;
white-space: pre; white-space: pre;
......
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