Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
R
react-ag-qeditor
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lib
react-ag-qeditor
Commits
68e5acd7
Commit
68e5acd7
authored
Feb 02, 2026
by
Яков
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update iframe and video
parent
ffd1d840
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
757 additions
and
109 deletions
+757
-109
pdf.svg
pdf.svg
+40
-0
ToolBar.js
src/components/ToolBar.js
+1
-0
Iframe.js
src/extensions/Iframe.js
+360
-59
Video.js
src/extensions/Video.js
+356
-50
No files found.
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 }) => {
...
@@ -257,6 +257,7 @@ const ToolBar = ({ editor, toolsLib = [], toolsOptions }) => {
}
}
}
}
const
getItems
=
()
=>
{
const
getItems
=
()
=>
{
let
toolItems
=
[];
let
toolItems
=
[];
...
...
src/extensions/Iframe.js
View file @
68e5acd7
import
React
,
{
Fragment
,
useEffect
,
useRef
,
useState
}
from
'react'
import
{
Node
,
mergeAttributes
}
from
'@tiptap/core'
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
({
const
Iframe
=
Node
.
create
({
name
:
'iframe'
,
name
:
'iframe'
,
group
:
'block'
,
group
:
'block'
,
selectable
:
fals
e
,
selectable
:
tru
e
,
draggable
:
true
,
draggable
:
true
,
atom
:
true
,
atom
:
true
,
addAttributes
()
{
addAttributes
()
{
return
{
return
{
src
:
{
src
:
{
default
:
null
},
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
},
},
frameborder
:
{
renderHTML
:
(
attrs
)
=>
(
attrs
.
width
?
{
width
:
attrs
.
width
}
:
{}),
default
:
0
},
allowfullscreen
:
{
default
:
true
,
parseHTML
:
()
=>
{
// console.log(this)
}
}
}
},
},
parseHTML
()
{
height
:
{
return
[
default
:
null
,
{
parseHTML
:
(
el
)
=>
{
tag
:
'iframe'
const
v
=
parseInt
(
el
.
getAttribute
(
'height'
)
||
''
,
10
)
}
return
Number
.
isFinite
(
v
)
?
v
:
null
]
},
},
renderHTML
:
(
attrs
)
=>
(
attrs
.
height
?
{
height
:
attrs
.
height
}
:
{}),
renderHTML
({
HTMLAttributes
})
{
HTMLAttributes
.
allowfullscreen
=
1
;
HTMLAttributes
.
allow
=
"fullscreen"
return
[
'iframe'
,
mergeAttributes
(
HTMLAttributes
)]
},
},
addNodeView
()
{
align
:
{
return
({
editor
,
node
,
...
a
})
=>
{
default
:
'left'
,
const
container
=
document
.
createElement
(
'div'
)
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'data-align'
)
||
'left'
,
renderHTML
:
(
attrs
)
=>
({
'data-align'
:
attrs
.
align
}),
const
iframe
=
document
.
createElement
(
'iframe'
)
},
iframe
.
src
=
node
.
attrs
.
src
iframe
.
allowfullscreen
=
node
.
attrs
.
allowfullscreen
iframe
.
classList
.
add
(
'customIframe'
)
const
closeBtn
=
document
.
createElement
(
'button'
)
'data-node-id'
:
{
closeBtn
.
textContent
=
'X'
default
:
null
,
closeBtn
.
classList
.
add
(
'closeBtn'
)
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'data-node-id'
),
renderHTML
:
(
attrs
)
=>
({
'data-node-id'
:
attrs
[
'data-node-id'
]
}),
},
}
},
closeBtn
.
addEventListener
(
'click'
,
function
()
{
parseHTML
()
{
const
pos
=
editor
.
view
.
posAtDOM
(
container
,
0
)
return
[{
tag
:
'iframe'
}]
editor
.
view
.
dispatch
(
},
editor
.
view
.
state
.
tr
.
delete
(
pos
,
pos
+
node
.
nodeSize
)
)
})
// if (editor.isEditable) {
renderHTML
({
node
,
HTMLAttributes
})
{
// container.classList.add('pointer-events-none');
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
{
addNodeView
()
{
dom
:
container
return
ReactNodeViewRenderer
(
ResizableIframeView
)
}
}
},
},
addCommands
()
{
addCommands
()
{
return
{
return
{
setIframe
:
setIframe
:
(
options
)
=>
(
options
)
=>
({
tr
,
dispatch
})
=>
{
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
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
return
true
},
}
}
}
},
}
})
})
export
default
Iframe
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
{
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
({
const
Video
=
Node
.
create
({
name
:
'video'
,
name
:
'video'
,
group
:
'block'
,
group
:
'block'
,
selectable
:
fals
e
,
selectable
:
tru
e
,
draggable
:
true
,
draggable
:
true
,
atom
:
true
,
atom
:
true
,
addAttributes
()
{
addAttributes
()
{
return
{
return
{
"src"
:
{
src
:
{
default
:
null
},
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
},
},
"poster"
:
{
renderHTML
:
(
attrs
)
=>
(
attrs
.
width
?
{
width
:
attrs
.
width
}
:
{}),
default
:
null
},
},
"controls"
:
{
default
:
true
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
}
:
{}),
},
},
parseHTML
()
{
align
:
{
return
[
default
:
'left'
,
{
parseHTML
:
(
el
)
=>
el
.
getAttribute
(
'data-align'
)
||
'left'
,
tag
:
'video'
,
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'
]
}),
},
}
},
},
renderHTML
({
HTMLAttributes
}
)
{
parseHTML
(
)
{
return
[
'video'
,
mergeAttributes
(
HTMLAttributes
)];
return
[
{
tag
:
'video'
}]
},
},
addNodeView
()
{
renderHTML
({
node
,
HTMLAttributes
})
{
return
({
editor
,
node
})
=>
{
const
align
=
node
.
attrs
.
align
||
'left'
const
container
=
document
.
createElement
(
'div'
);
const
style
=
getStyleForAlign
(
align
)
if
(
node
.
attrs
.
width
)
style
.
push
(
`width:
${
node
.
attrs
.
width
}
px`
)
const
video
=
document
.
createElement
(
'video'
);
if
(
node
.
attrs
.
height
)
style
.
push
(
`height:
${
node
.
attrs
.
height
}
px`
)
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
[
'video'
,
mergeAttributes
(
HTMLAttributes
,
{
controls
:
node
.
attrs
.
controls
!==
false
?
1
:
null
,
'data-align'
:
align
,
style
:
style
.
join
(
'; '
),
}),
]
},
return
{
addNodeView
()
{
dom
:
container
,
return
ReactNodeViewRenderer
(
ResizableVideoView
)
}
}
},
},
addCommands
()
{
addCommands
()
{
return
{
return
{
setVideo
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
setVideo
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
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
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