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
68e5acd7
Commit
68e5acd7
authored
Feb 02, 2026
by
Яков
Browse files
update iframe and video
parent
ffd1d840
Changes
4
Hide whitespace changes
Inline
Side-by-side
pdf.svg
0 → 100644
View file @
68e5acd7
<svg
width=
"256"
height=
"160"
viewBox=
"0 0 256 160"
xmlns=
"http://www.w3.org/2000/svg"
>
<style>
.stroke { stroke:#000; stroke-width:4; fill:none; stroke-linejoin:round; stroke-linecap:round; }
.fill { fill:#000; }
.text { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill:#000; font-weight:bold; }
</style>
<!-- Левый документ (PDF) -->
<rect
x=
"16"
y=
"16"
width=
"80"
height=
"96"
rx=
"6"
class=
"stroke"
/>
<polyline
points=
"76,16 96,16 96,36"
class=
"stroke"
/>
<rect
x=
"28"
y=
"36"
width=
"56"
height=
"22"
class=
"fill"
rx=
"3"
/>
<text
x=
"56"
y=
"52"
text-anchor=
"middle"
class=
"text"
font-size=
"14"
fill=
"#fff"
>
PDF
</text>
<line
x1=
"28"
y1=
"68"
x2=
"88"
y2=
"68"
class=
"stroke"
/>
<line
x1=
"28"
y1=
"80"
x2=
"74"
y2=
"80"
class=
"stroke"
/>
<line
x1=
"28"
y1=
"92"
x2=
"64"
y2=
"92"
class=
"stroke"
/>
<!-- Стрелка (слева направо) -->
<polygon
class=
"fill"
points=
"
112,64 132,64
132,54 160,72
132,90 132,80
112,80
"
/>
<!-- Правый документ (TEXT) -->
<rect
x=
"160"
y=
"16"
width=
"80"
height=
"96"
rx=
"6"
class=
"stroke"
/>
<polyline
points=
"220,16 240,16 240,36"
class=
"stroke"
/>
<line
x1=
"172"
y1=
"36"
x2=
"232"
y2=
"36"
class=
"stroke"
/>
<line
x1=
"172"
y1=
"48"
x2=
"228"
y2=
"48"
class=
"stroke"
/>
<line
x1=
"172"
y1=
"60"
x2=
"224"
y2=
"60"
class=
"stroke"
/>
<line
x1=
"172"
y1=
"72"
x2=
"232"
y2=
"72"
class=
"stroke"
/>
<line
x1=
"172"
y1=
"84"
x2=
"220"
y2=
"84"
class=
"stroke"
/>
<!-- Подпись снизу -->
<text
x=
"128"
y=
"146"
text-anchor=
"middle"
class=
"text"
font-size=
"18"
>
PDF -> TEXT
</text>
</svg>
src/components/ToolBar.js
View file @
68e5acd7
...
...
@@ -257,6 +257,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions }) => {
}
}
const
getItems
=
()
=>
{
let
toolItems
=
[];
...
...
src/extensions/Iframe.js
View file @
68e5acd7
import
React
,
{
Fragment
,
useEffect
,
useRef
,
useState
}
from
'
react
'
import
{
Node
,
mergeAttributes
}
from
'
@tiptap/core
'
import
{
NodeViewWrapper
,
ReactNodeViewRenderer
}
from
'
@tiptap/react
'
const
MIN_WIDTH
=
160
const
BORDER_COLOR
=
'
#0096fd
'
const
ALIGN_OPTIONS
=
[
'
left
'
,
'
center
'
,
'
right
'
]
const
getStyleForAlign
=
(
align
)
=>
{
const
style
=
[]
if
(
align
===
'
center
'
)
{
style
.
push
(
'
display: block
'
,
'
margin-left: auto
'
,
'
margin-right: auto
'
)
}
else
if
(
align
===
'
left
'
)
{
style
.
push
(
'
float: left
'
,
'
margin-right: 1rem
'
)
}
else
if
(
align
===
'
right
'
)
{
style
.
push
(
'
float: right
'
,
'
margin-left: 1rem
'
)
}
return
style
}
const
ResizableIframeView
=
({
editor
,
node
,
updateAttributes
,
getPos
,
selected
})
=>
{
const
wrapperRef
=
useRef
(
null
)
const
iframeRef
=
useRef
(
null
)
const
[
showAlignMenu
,
setShowAlignMenu
]
=
useState
(
false
)
const
[
isResizing
,
setIsResizing
]
=
useState
(
false
)
const
isInitialized
=
useRef
(
false
)
const
getEditorDimensions
=
()
=>
{
const
editorContent
=
editor
?.
options
?.
element
?.
closest
(
'
.atma-editor-content
'
)
if
(
!
editorContent
)
return
{
width
:
Infinity
,
availableSpace
:
Infinity
}
const
fullEditorWidth
=
editorContent
.
clientWidth
const
editorStyles
=
window
.
getComputedStyle
(
editorContent
)
const
paddingLeft
=
parseFloat
(
editorStyles
.
paddingLeft
)
||
0
const
paddingRight
=
parseFloat
(
editorStyles
.
paddingRight
)
||
0
const
availableEditorWidth
=
fullEditorWidth
-
paddingLeft
-
paddingRight
const
container
=
wrapperRef
.
current
?.
closest
(
'
li, blockquote, td, p, div
'
)
||
editorContent
const
containerStyles
=
window
.
getComputedStyle
(
container
)
const
containerPaddingLeft
=
parseFloat
(
containerStyles
.
paddingLeft
)
||
0
const
containerPaddingRight
=
parseFloat
(
containerStyles
.
paddingRight
)
||
0
const
containerWidth
=
container
.
clientWidth
-
containerPaddingLeft
-
containerPaddingRight
return
{
width
:
containerWidth
,
availableSpace
:
availableEditorWidth
}
}
const
safeUpdateAttributes
=
(
newAttrs
)
=>
{
const
{
width
:
containerWidth
,
availableSpace
}
=
getEditorDimensions
()
let
width
=
newAttrs
.
width
??
node
.
attrs
.
width
let
height
=
newAttrs
.
height
??
node
.
attrs
.
height
if
(
typeof
width
===
'
number
'
&&
typeof
height
===
'
number
'
)
{
const
maxWidth
=
node
.
attrs
.
align
===
'
center
'
?
containerWidth
:
availableSpace
if
(
width
>
maxWidth
)
{
const
ratio
=
maxWidth
/
width
width
=
maxWidth
height
=
Math
.
round
(
height
*
ratio
)
}
if
(
width
<
MIN_WIDTH
)
{
const
ratio
=
MIN_WIDTH
/
width
width
=
MIN_WIDTH
height
=
Math
.
round
(
height
*
ratio
)
}
}
updateAttributes
({
...
newAttrs
,
width
,
height
})
}
useEffect
(()
=>
{
if
(
!
node
.
attrs
[
'
data-node-id
'
])
{
safeUpdateAttributes
({
'
data-node-id
'
:
`iframe-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
slice
(
2
,
9
)}
`
,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
node
.
attrs
[
'
data-node-id
'
]])
useEffect
(()
=>
{
if
(
!
iframeRef
.
current
||
isInitialized
.
current
)
return
if
(
node
.
attrs
.
width
&&
node
.
attrs
.
height
)
{
isInitialized
.
current
=
true
return
}
const
{
width
:
editorWidth
}
=
getEditorDimensions
()
const
initialWidth
=
Math
.
min
(
editorWidth
,
720
)
const
initialHeight
=
Math
.
round
((
initialWidth
*
9
)
/
16
)
safeUpdateAttributes
({
width
:
initialWidth
,
height
:
initialHeight
,
align
:
node
.
attrs
.
align
||
'
left
'
,
})
isInitialized
.
current
=
true
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
node
.
attrs
.
width
,
node
.
attrs
.
height
])
const
handleResizeStart
=
(
dir
)
=>
(
e
)
=>
{
e
.
preventDefault
()
e
.
stopPropagation
()
setIsResizing
(
true
)
try
{
const
pos
=
getPos
?.()
if
(
typeof
pos
===
'
number
'
)
editor
.
commands
.
setNodeSelection
(
pos
)
}
catch
(
err
)
{
console
.
warn
(
'
getPos() failed:
'
,
err
)
}
const
startWidth
=
node
.
attrs
.
width
||
iframeRef
.
current
?.
clientWidth
||
560
const
startHeight
=
node
.
attrs
.
height
||
iframeRef
.
current
?.
clientHeight
||
315
const
aspectRatio
=
startWidth
/
startHeight
const
startX
=
e
.
clientX
const
startY
=
e
.
clientY
const
{
width
:
containerWidth
,
availableSpace
}
=
getEditorDimensions
()
const
maxWidth
=
node
.
attrs
.
align
===
'
center
'
?
containerWidth
:
availableSpace
const
onMouseMove
=
(
ev
)
=>
{
requestAnimationFrame
(()
=>
{
const
deltaX
=
ev
.
clientX
-
startX
const
deltaY
=
ev
.
clientY
-
startY
let
newWidth
=
startWidth
if
(
dir
.
includes
(
'
e
'
))
newWidth
=
startWidth
+
deltaX
if
(
dir
.
includes
(
'
w
'
))
newWidth
=
startWidth
-
deltaX
if
(
!
dir
.
includes
(
'
e
'
)
&&
!
dir
.
includes
(
'
w
'
))
{
const
newHeight
=
startHeight
+
(
dir
.
includes
(
'
s
'
)
?
deltaY
:
-
deltaY
)
newWidth
=
newHeight
*
aspectRatio
}
newWidth
=
Math
.
max
(
MIN_WIDTH
,
Math
.
min
(
maxWidth
,
newWidth
))
const
newHeight
=
Math
.
round
(
newWidth
/
aspectRatio
)
safeUpdateAttributes
({
width
:
Math
.
round
(
newWidth
),
height
:
newHeight
})
})
}
const
onMouseUp
=
()
=>
{
setIsResizing
(
false
)
document
.
removeEventListener
(
'
mousemove
'
,
onMouseMove
)
document
.
removeEventListener
(
'
mouseup
'
,
onMouseUp
)
}
document
.
addEventListener
(
'
mousemove
'
,
onMouseMove
)
document
.
addEventListener
(
'
mouseup
'
,
onMouseUp
)
}
const
handleAlign
=
(
align
)
=>
{
safeUpdateAttributes
({
align
})
setShowAlignMenu
(
false
)
}
const
deleteNode
=
(
e
)
=>
{
e
.
preventDefault
()
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
(
'
getPos() failed:
'
,
err
)
}
}
const
wrapperStyle
=
{
position
:
'
relative
'
,
display
:
node
.
attrs
.
align
===
'
center
'
?
'
block
'
:
'
inline-block
'
,
width
:
node
.
attrs
.
width
?
`
${
node
.
attrs
.
width
}
px`
:
undefined
,
height
:
node
.
attrs
.
height
?
`
${
node
.
attrs
.
height
}
px`
:
undefined
,
}
return
(
<
NodeViewWrapper
ref
=
{
wrapperRef
}
as
=
"
div
"
className
=
"
atma-iframe-wrapper
"
style
=
{
wrapperStyle
}
data
-
align
=
{
node
.
attrs
.
align
||
'
left
'
}
>
<
iframe
ref
=
{
iframeRef
}
src
=
{
node
.
attrs
.
src
}
frameBorder
=
{
node
.
attrs
.
frameborder
??
0
}
allowFullScreen
allow
=
"
fullscreen
"
style
=
{{
width
:
'
100%
'
,
height
:
'
100%
'
,
pointerEvents
:
editor
.
isEditable
?
'
none
'
:
'
auto
'
,
}}
/
>
{(
selected
||
isResizing
)
&&
(
<
Fragment
>
<
button
type
=
"
button
"
onClick
=
{
deleteNode
}
style
=
{{
position
:
'
absolute
'
,
top
:
4
,
right
:
4
,
zIndex
:
30
,
backgroundColor
:
'
white
'
,
border
:
'
1px solid #d9d9d9
'
,
borderRadius
:
'
50%
'
,
width
:
20
,
height
:
20
,
fontSize
:
12
,
lineHeight
:
1
,
padding
:
'
0px 0px 2px 0px
'
,
cursor
:
'
pointer
'
,
}}
>
×
<
/button
>
{[
'
nw
'
,
'
ne
'
,
'
sw
'
,
'
se
'
].
map
((
d
)
=>
(
<
div
key
=
{
d
}
onMouseDown
=
{
handleResizeStart
(
d
)}
style
=
{{
position
:
'
absolute
'
,
width
:
12
,
height
:
12
,
backgroundColor
:
BORDER_COLOR
,
border
:
'
1px solid white
'
,
[
d
[
0
]
===
'
n
'
?
'
top
'
:
'
bottom
'
]:
-
6
,
[
d
[
1
]
===
'
w
'
?
'
left
'
:
'
right
'
]:
-
6
,
cursor
:
`
${
d
}
-resize`
,
zIndex
:
10
,
}}
/
>
))}
{
showAlignMenu
&&
(
<
div
style
=
{{
position
:
'
absolute
'
,
top
:
-
40
,
left
:
'
50%
'
,
transform
:
'
translateX(-50%)
'
,
backgroundColor
:
'
white
'
,
boxShadow
:
'
0 2px 8px rgba(0,0,0,0.15)
'
,
borderRadius
:
4
,
padding
:
4
,
zIndex
:
20
,
display
:
'
flex
'
,
}}
>
{
ALIGN_OPTIONS
.
map
((
align
)
=>
(
<
button
type
=
"
button
"
key
=
{
align
}
onClick
=
{()
=>
handleAlign
(
align
)}
style
=
{{
margin
:
'
0 2px
'
,
padding
:
'
10px 8px
'
,
background
:
node
.
attrs
.
align
===
align
?
'
#e6f7ff
'
:
'
transparent
'
,
border
:
'
1px solid #d9d9d9
'
,
borderRadius
:
2
,
cursor
:
'
pointer
'
,
}}
>
{
align
}
<
/button
>
))}
<
/div
>
)}
<
button
type
=
"
button
"
onClick
=
{(
e
)
=>
{
e
.
stopPropagation
()
setShowAlignMenu
((
v
)
=>
!
v
)
}}
style
=
{{
position
:
'
absolute
'
,
top
:
-
30
,
left
:
'
calc(50% - 6px)
'
,
transform
:
'
translateX(-50%)
'
,
backgroundColor
:
'
white
'
,
border
:
`1px solid
${
BORDER_COLOR
}
`
,
borderRadius
:
4
,
padding
:
'
8px 8px
'
,
cursor
:
'
pointer
'
,
fontSize
:
12
,
zIndex
:
10
,
}}
>
Align
<
/button
>
<
/Fragment
>
)}
<
/NodeViewWrapper
>
)
}
const
Iframe
=
Node
.
create
({
name
:
'
iframe
'
,
group
:
'
block
'
,
selectable
:
fals
e
,
selectable
:
tru
e
,
draggable
:
true
,
atom
:
true
,
addAttributes
()
{
addAttributes
()
{
return
{
src
:
{
default
:
null
src
:
{
default
:
null
},
frameborder
:
{
default
:
0
},
allowfullscreen
:
{
default
:
true
},
width
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
{
const
v
=
parseInt
(
el
.
getAttribute
(
'
width
'
)
||
''
,
10
)
return
Number
.
isFinite
(
v
)
?
v
:
null
},
renderHTML
:
(
attrs
)
=>
(
attrs
.
width
?
{
width
:
attrs
.
width
}
:
{}),
},
frameborder
:
{
default
:
0
height
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
{
const
v
=
parseInt
(
el
.
getAttribute
(
'
height
'
)
||
''
,
10
)
return
Number
.
isFinite
(
v
)
?
v
:
null
},
renderHTML
:
(
attrs
)
=>
(
attrs
.
height
?
{
height
:
attrs
.
height
}
:
{}),
},
allowfullscreen
:
{
default
:
true
,
parseHTML
:
()
=>
{
// console.log(this)
}
}
}
},
parseHTML
()
{
return
[
{
tag
:
'
iframe
'
}
]
},
align
:
{
default
:
'
left
'
,
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'
data-align
'
)
||
'
left
'
,
renderHTML
:
(
attrs
)
=>
({
'
data-align
'
:
attrs
.
align
}),
},
renderHTML
({
HTMLAttributes
})
{
HTMLAttributes
.
allowfullscreen
=
1
;
HTMLAttributes
.
allow
=
"
fullscreen
"
return
[
'
iframe
'
,
mergeAttributes
(
HTMLAttributes
)]
'
data-node-id
'
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'
data-node-id
'
),
renderHTML
:
(
attrs
)
=>
({
'
data-node-id
'
:
attrs
[
'
data-node-id
'
]
}),
},
}
},
addNodeView
()
{
return
({
editor
,
node
,
...
a
})
=>
{
const
container
=
document
.
createElement
(
'
div
'
)
const
iframe
=
document
.
createElement
(
'
iframe
'
)
iframe
.
src
=
node
.
attrs
.
src
iframe
.
allowfullscreen
=
node
.
attrs
.
allowfullscreen
iframe
.
classList
.
add
(
'
customIframe
'
)
const
closeBtn
=
document
.
createElement
(
'
button
'
)
closeBtn
.
textContent
=
'
X
'
closeBtn
.
classList
.
add
(
'
closeBtn
'
)
closeBtn
.
addEventListener
(
'
click
'
,
function
()
{
const
pos
=
editor
.
view
.
posAtDOM
(
container
,
0
)
editor
.
view
.
dispatch
(
editor
.
view
.
state
.
tr
.
delete
(
pos
,
pos
+
node
.
nodeSize
)
)
})
parseHTML
()
{
return
[{
tag
:
'
iframe
'
}]
},
// if (editor.isEditable) {
// container.classList.add('pointer-events-none');
// }
renderHTML
({
node
,
HTMLAttributes
})
{
const
align
=
node
.
attrs
.
align
||
'
left
'
const
style
=
getStyleForAlign
(
align
)
if
(
node
.
attrs
.
width
)
style
.
push
(
`width:
${
node
.
attrs
.
width
}
px`
)
if
(
node
.
attrs
.
height
)
style
.
push
(
`height:
${
node
.
attrs
.
height
}
px`
)
container
.
append
(
closeBtn
,
iframe
)
return
[
'
iframe
'
,
mergeAttributes
(
HTMLAttributes
,
{
allowfullscreen
:
1
,
allow
:
'
fullscreen
'
,
frameborder
:
node
.
attrs
.
frameborder
??
0
,
'
data-align
'
:
align
,
style
:
style
.
join
(
'
;
'
),
}),
]
},
return
{
dom
:
container
}
}
addNodeView
()
{
return
ReactNodeViewRenderer
(
ResizableIframeView
)
},
addCommands
()
{
addCommands
()
{
return
{
setIframe
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
if
(
dispatch
)
{
tr
.
replaceRangeWith
(
selection
.
from
,
selection
.
to
,
node
)
}
if
(
dispatch
)
tr
.
replaceRangeWith
(
selection
.
from
,
selection
.
to
,
node
)
return
true
}
}
,
}
}
}
,
})
export
default
Iframe
src/extensions/Video.js
View file @
68e5acd7
import
React
,
{
Fragment
,
useEffect
,
useRef
,
useState
}
from
'
react
'
import
{
Node
,
mergeAttributes
}
from
'
@tiptap/core
'
import
{
NodeViewWrapper
,
ReactNodeViewRenderer
}
from
'
@tiptap/react
'
const
MIN_WIDTH
=
200
const
BORDER_COLOR
=
'
#0096fd
'
const
ALIGN_OPTIONS
=
[
'
left
'
,
'
center
'
,
'
right
'
]
const
getStyleForAlign
=
(
align
)
=>
{
const
style
=
[]
if
(
align
===
'
center
'
)
{
style
.
push
(
'
display: block
'
,
'
margin-left: auto
'
,
'
margin-right: auto
'
)
}
else
if
(
align
===
'
left
'
)
{
style
.
push
(
'
float: left
'
,
'
margin-right: 1rem
'
)
}
else
if
(
align
===
'
right
'
)
{
style
.
push
(
'
float: right
'
,
'
margin-left: 1rem
'
)
}
return
style
}
const
ResizableVideoView
=
({
editor
,
node
,
updateAttributes
,
getPos
,
selected
})
=>
{
const
wrapperRef
=
useRef
(
null
)
const
videoRef
=
useRef
(
null
)
const
[
showAlignMenu
,
setShowAlignMenu
]
=
useState
(
false
)
const
[
isResizing
,
setIsResizing
]
=
useState
(
false
)
const
isInitialized
=
useRef
(
false
)
const
getEditorDimensions
=
()
=>
{
const
editorContent
=
editor
?.
options
?.
element
?.
closest
(
'
.atma-editor-content
'
)
if
(
!
editorContent
)
return
{
width
:
Infinity
,
availableSpace
:
Infinity
}
const
fullEditorWidth
=
editorContent
.
clientWidth
const
editorStyles
=
window
.
getComputedStyle
(
editorContent
)
const
paddingLeft
=
parseFloat
(
editorStyles
.
paddingLeft
)
||
0
const
paddingRight
=
parseFloat
(
editorStyles
.
paddingRight
)
||
0
const
availableEditorWidth
=
fullEditorWidth
-
paddingLeft
-
paddingRight
const
container
=
wrapperRef
.
current
?.
closest
(
'
li, blockquote, td, p, div
'
)
||
editorContent
const
containerStyles
=
window
.
getComputedStyle
(
container
)
const
containerPaddingLeft
=
parseFloat
(
containerStyles
.
paddingLeft
)
||
0
const
containerPaddingRight
=
parseFloat
(
containerStyles
.
paddingRight
)
||
0
const
containerWidth
=
container
.
clientWidth
-
containerPaddingLeft
-
containerPaddingRight
return
{
width
:
containerWidth
,
availableSpace
:
availableEditorWidth
}
}
const
safeUpdateAttributes
=
(
newAttrs
)
=>
{
const
{
width
:
containerWidth
,
availableSpace
}
=
getEditorDimensions
()
let
width
=
newAttrs
.
width
??
node
.
attrs
.
width
let
height
=
newAttrs
.
height
??
node
.
attrs
.
height
if
(
typeof
width
===
'
number
'
&&
typeof
height
===
'
number
'
)
{
const
maxWidth
=
node
.
attrs
.
align
===
'
center
'
?
containerWidth
:
availableSpace
if
(
width
>
maxWidth
)
{
const
ratio
=
maxWidth
/
width
width
=
maxWidth
height
=
Math
.
round
(
height
*
ratio
)
}
if
(
width
<
MIN_WIDTH
)
{
const
ratio
=
MIN_WIDTH
/
width
width
=
MIN_WIDTH
height
=
Math
.
round
(
height
*
ratio
)
}
}
updateAttributes
({
...
newAttrs
,
width
,
height
})
}
useEffect
(()
=>
{
if
(
!
node
.
attrs
[
'
data-node-id
'
])
{
safeUpdateAttributes
({
'
data-node-id
'
:
`video-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
slice
(
2
,
9
)}
`
,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
node
.
attrs
[
'
data-node-id
'
]])
useEffect
(()
=>
{
if
(
!
videoRef
.
current
||
isInitialized
.
current
)
return
if
(
node
.
attrs
.
width
&&
node
.
attrs
.
height
)
{
isInitialized
.
current
=
true
return
}
const
{
width
:
editorWidth
}
=
getEditorDimensions
()
const
initialWidth
=
Math
.
min
(
editorWidth
,
720
)
const
initialHeight
=
Math
.
round
((
initialWidth
*
9
)
/
16
)
safeUpdateAttributes
({
width
:
initialWidth
,
height
:
initialHeight
,
align
:
node
.
attrs
.
align
||
'
left
'
,
})
isInitialized
.
current
=
true
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[
node
.
attrs
.
width
,
node
.
attrs
.
height
])
const
handleResizeStart
=
(
dir
)
=>
(
e
)
=>
{
e
.
preventDefault
()
e
.
stopPropagation
()
setIsResizing
(
true
)
try
{
const
pos
=
getPos
?.()
if
(
typeof
pos
===
'
number
'
)
editor
.
commands
.
setNodeSelection
(
pos
)
}
catch
(
err
)
{
console
.
warn
(
'
getPos() failed:
'
,
err
)
}
const
startWidth
=
node
.
attrs
.
width
||
videoRef
.
current
?.
clientWidth
||
640
const
startHeight
=
node
.
attrs
.
height
||
videoRef
.
current
?.
clientHeight
||
360
const
aspectRatio
=
startWidth
/
startHeight
const
startX
=
e
.
clientX
const
startY
=
e
.
clientY
const
{
width
:
containerWidth
,
availableSpace
}
=
getEditorDimensions
()
const
maxWidth
=
node
.
attrs
.
align
===
'
center
'
?
containerWidth
:
availableSpace
const
onMouseMove
=
(
ev
)
=>
{
requestAnimationFrame
(()
=>
{
const
deltaX
=
ev
.
clientX
-
startX
const
deltaY
=
ev
.
clientY
-
startY
let
newWidth
=
startWidth
if
(
dir
.
includes
(
'
e
'
))
newWidth
=
startWidth
+
deltaX
if
(
dir
.
includes
(
'
w
'
))
newWidth
=
startWidth
-
deltaX
if
(
!
dir
.
includes
(
'
e
'
)
&&
!
dir
.
includes
(
'
w
'
))
{
const
newHeight
=
startHeight
+
(
dir
.
includes
(
'
s
'
)
?
deltaY
:
-
deltaY
)
newWidth
=
newHeight
*
aspectRatio
}
newWidth
=
Math
.
max
(
MIN_WIDTH
,
Math
.
min
(
maxWidth
,
newWidth
))
const
newHeight
=
Math
.
round
(
newWidth
/
aspectRatio
)
safeUpdateAttributes
({
width
:
Math
.
round
(
newWidth
),
height
:
newHeight
})
})
}
const
onMouseUp
=
()
=>
{
setIsResizing
(
false
)
document
.
removeEventListener
(
'
mousemove
'
,
onMouseMove
)
document
.
removeEventListener
(
'
mouseup
'
,
onMouseUp
)
}
document
.
addEventListener
(
'
mousemove
'
,
onMouseMove
)
document
.
addEventListener
(
'
mouseup
'
,
onMouseUp
)
}
const
handleAlign
=
(
align
)
=>
{
safeUpdateAttributes
({
align
})
setShowAlignMenu
(
false
)
}
const
deleteNode
=
(
e
)
=>
{
e
.
preventDefault
()
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
(
'
getPos() failed:
'
,
err
)
}
}
const
wrapperStyle
=
{
position
:
'
relative
'
,
display
:
node
.
attrs
.
align
===
'
center
'
?
'
block
'
:
'
inline-block
'
,
width
:
node
.
attrs
.
width
?
`
${
node
.
attrs
.
width
}
px`
:
undefined
,
height
:
node
.
attrs
.
height
?
`
${
node
.
attrs
.
height
}
px`
:
undefined
,
}
return
(
<
NodeViewWrapper
ref
=
{
wrapperRef
}
as
=
"
div
"
className
=
"
atma-video-wrapper
"
style
=
{
wrapperStyle
}
data
-
align
=
{
node
.
attrs
.
align
||
'
left
'
}
>
<
video
ref
=
{
videoRef
}
src
=
{
node
.
attrs
.
src
}
poster
=
{
node
.
attrs
.
poster
}
controls
=
{
node
.
attrs
.
controls
!==
false
}
style
=
{{
width
:
'
100%
'
,
height
:
'
100%
'
,
pointerEvents
:
editor
.
isEditable
?
'
none
'
:
'
auto
'
,
}}
/
>
{(
selected
||
isResizing
)
&&
(
<
Fragment
>
<
button
type
=
"
button
"
onClick
=
{
deleteNode
}
style
=
{{
position
:
'
absolute
'
,
top
:
4
,
right
:
4
,
zIndex
:
30
,
backgroundColor
:
'
white
'
,
border
:
'
1px solid #d9d9d9
'
,
borderRadius
:
'
50%
'
,
width
:
20
,
height
:
20
,
fontSize
:
12
,
lineHeight
:
1
,
padding
:
'
0px 0px 2px 0px
'
,
cursor
:
'
pointer
'
,
}}
>
×
<
/button
>
{[
'
nw
'
,
'
ne
'
,
'
sw
'
,
'
se
'
].
map
((
d
)
=>
(
<
div
key
=
{
d
}
onMouseDown
=
{
handleResizeStart
(
d
)}
style
=
{{
position
:
'
absolute
'
,
width
:
12
,
height
:
12
,
backgroundColor
:
BORDER_COLOR
,
border
:
'
1px solid white
'
,
[
d
[
0
]
===
'
n
'
?
'
top
'
:
'
bottom
'
]:
-
6
,
[
d
[
1
]
===
'
w
'
?
'
left
'
:
'
right
'
]:
-
6
,
cursor
:
`
${
d
}
-resize`
,
zIndex
:
10
,
}}
/
>
))}
{
showAlignMenu
&&
(
<
div
style
=
{{
position
:
'
absolute
'
,
top
:
-
40
,
left
:
'
50%
'
,
transform
:
'
translateX(-50%)
'
,
backgroundColor
:
'
white
'
,
boxShadow
:
'
0 2px 8px rgba(0,0,0,0.15)
'
,
borderRadius
:
4
,
padding
:
4
,
zIndex
:
20
,
display
:
'
flex
'
,
}}
>
{
ALIGN_OPTIONS
.
map
((
align
)
=>
(
<
button
type
=
"
button
"
key
=
{
align
}
onClick
=
{()
=>
handleAlign
(
align
)}
style
=
{{
margin
:
'
0 2px
'
,
padding
:
'
10px 8px
'
,
background
:
node
.
attrs
.
align
===
align
?
'
#e6f7ff
'
:
'
transparent
'
,
border
:
'
1px solid #d9d9d9
'
,
borderRadius
:
2
,
cursor
:
'
pointer
'
,
}}
>
{
align
}
<
/button
>
))}
<
/div
>
)}
<
button
type
=
"
button
"
onClick
=
{(
e
)
=>
{
e
.
stopPropagation
()
setShowAlignMenu
((
v
)
=>
!
v
)
}}
style
=
{{
position
:
'
absolute
'
,
top
:
-
30
,
left
:
'
calc(50% - 6px)
'
,
transform
:
'
translateX(-50%)
'
,
backgroundColor
:
'
white
'
,
border
:
`1px solid
${
BORDER_COLOR
}
`
,
borderRadius
:
4
,
padding
:
'
8px 8px
'
,
cursor
:
'
pointer
'
,
fontSize
:
12
,
zIndex
:
10
,
}}
>
Align
<
/button
>
<
/Fragment
>
)}
<
/NodeViewWrapper
>
)
}
const
Video
=
Node
.
create
({
name
:
'
video
'
,
group
:
'
block
'
,
selectable
:
fals
e
,
selectable
:
tru
e
,
draggable
:
true
,
atom
:
true
,
addAttributes
()
{
return
{
"
src
"
:
{
default
:
null
src
:
{
default
:
null
},
poster
:
{
default
:
null
},
controls
:
{
default
:
true
},
width
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
{
const
v
=
parseInt
(
el
.
getAttribute
(
'
width
'
)
||
''
,
10
)
return
Number
.
isFinite
(
v
)
?
v
:
null
},
renderHTML
:
(
attrs
)
=>
(
attrs
.
width
?
{
width
:
attrs
.
width
}
:
{}),
},
"
poster
"
:
{
default
:
null
height
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
{
const
v
=
parseInt
(
el
.
getAttribute
(
'
height
'
)
||
''
,
10
)
return
Number
.
isFinite
(
v
)
?
v
:
null
},
renderHTML
:
(
attrs
)
=>
(
attrs
.
height
?
{
height
:
attrs
.
height
}
:
{}),
},
align
:
{
default
:
'
left
'
,
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'
data-align
'
)
||
'
left
'
,
renderHTML
:
(
attrs
)
=>
({
'
data-align
'
:
attrs
.
align
}),
},
'
data-node-id
'
:
{
default
:
null
,
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'
data-node-id
'
),
renderHTML
:
(
attrs
)
=>
({
'
data-node-id
'
:
attrs
[
'
data-node-id
'
]
}),
},
"
controls
"
:
{
default
:
true
}
}
},
parseHTML
()
{
return
[
{
tag
:
'
video
'
,
},
]
return
[{
tag
:
'
video
'
}]
},
renderHTML
({
HTMLAttributes
})
{
return
[
'
video
'
,
mergeAttributes
(
HTMLAttributes
)];
renderHTML
({
node
,
HTMLAttributes
})
{
const
align
=
node
.
attrs
.
align
||
'
left
'
const
style
=
getStyleForAlign
(
align
)
if
(
node
.
attrs
.
width
)
style
.
push
(
`width:
${
node
.
attrs
.
width
}
px`
)
if
(
node
.
attrs
.
height
)
style
.
push
(
`height:
${
node
.
attrs
.
height
}
px`
)
return
[
'
video
'
,
mergeAttributes
(
HTMLAttributes
,
{
controls
:
node
.
attrs
.
controls
!==
false
?
1
:
null
,
'
data-align
'
:
align
,
style
:
style
.
join
(
'
;
'
),
}),
]
},
addNodeView
()
{
return
({
editor
,
node
})
=>
{
const
container
=
document
.
createElement
(
'
div
'
);
const
video
=
document
.
createElement
(
'
video
'
);
if
(
editor
.
isEditable
)
{
video
.
className
=
'
pointer-events-none
'
;
}
video
.
src
=
node
.
attrs
.
src
;
video
.
poster
=
node
.
attrs
.
poster
;
video
.
controls
=
true
;
const
closeBtn
=
document
.
createElement
(
'
button
'
)
closeBtn
.
textContent
=
'
X
'
closeBtn
.
classList
.
add
(
'
closeBtn
'
)
closeBtn
.
addEventListener
(
'
click
'
,
function
()
{
const
pos
=
editor
.
view
.
posAtDOM
(
container
,
0
)
editor
.
view
.
dispatch
(
editor
.
view
.
state
.
tr
.
delete
(
pos
,
pos
+
node
.
nodeSize
)
)
})
container
.
append
(
closeBtn
,
video
)
return
{
dom
:
container
,
}
}
return
ReactNodeViewRenderer
(
ResizableVideoView
)
},
addCommands
()
{
return
{
setVideo
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
//
if
(
dispatch
)
{
tr
.
replaceRangeWith
(
selection
.
from
,
selection
.
to
,
node
)
}
return
true
},
setVideo
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
if
(
dispatch
)
tr
.
replaceRangeWith
(
selection
.
from
,
selection
.
to
,
node
)
return
true
},
}
},
})
;
})
export
default
Video
;
export
default
Video
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