Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
lib
react-ag-qeditor
Commits
66da5bdf
Commit
66da5bdf
authored
Jun 17, 2026
by
Яков
Browse files
update fix issue
parent
5d537563
Changes
3
Show whitespace changes
Inline
Side-by-side
package.json
View file @
66da5bdf
{
{
"name"
:
"react-ag-qeditor"
,
"name"
:
"react-ag-qeditor"
,
"version"
:
"1.1.7
5
"
,
"version"
:
"1.1.7
6
"
,
"description"
:
"WYSIWYG html editor"
,
"description"
:
"WYSIWYG html editor"
,
"author"
:
"atma"
,
"author"
:
"atma"
,
"license"
:
"
MIT
"
,
"license"
:
"
MIT
"
,
...
...
src/extensions/ToggleBlock.js
View file @
66da5bdf
...
@@ -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,13 +28,14 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
...
@@ -17,13 +28,14 @@ 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
}
>
{
selected
&&
(
<
button
<
button
type
=
"
button
"
type
=
"
button
"
onClick
=
{(
e
)
=>
{
onClick
=
{(
e
)
=>
{
...
@@ -45,7 +57,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
...
@@ -45,7 +57,7 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
right
:
4
,
right
:
4
,
zIndex
:
10
,
zIndex
:
10
,
background
:
'
white
'
,
background
:
'
white
'
,
border
:
'
1px solid #
ccc
'
,
border
:
'
1px solid #
d9d9d9
'
,
borderRadius
:
'
50%
'
,
borderRadius
:
'
50%
'
,
width
:
20
,
width
:
20
,
height
:
20
,
height
:
20
,
...
@@ -58,9 +70,18 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
...
@@ -58,9 +70,18 @@ export const ToggleBlockComponent = ({node, updateAttributes, getPos, editor}) =
>
>
×
×
<
/button
>
<
/button
>
)}
<
div
className
=
"
toggle-block-inner
"
>
<
div
className
=
"
toggle-block-inner
"
>
<
div
className
=
"
toggle-header-wrapper
"
>
<
div
className
=
"
toggle-header-wrapper
"
>
<
div
className
=
"
toggle-drag-handle
"
contentEditable
=
{
false
}
data
-
drag
-
handle
=
""
title
=
"
Перетащить
"
>
<
DragHandleIcon
/>
<
/div
>
<
span
<
span
className
=
"
toggle-header-measurer
"
className
=
"
toggle-header-measurer
"
ref
=
{
measurerRef
}
ref
=
{
measurerRef
}
...
@@ -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
(
'
toggleBlock
Ga
p
'
),
key
:
new
PluginKey
(
'
toggleBlock
Cleanu
p
'
),
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
])
}
}
...
...
src/index.scss
View file @
66da5bdf
...
@@ -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;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment