Commit 68e5acd7 by Яков

update iframe and video

parent ffd1d840
<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>
...@@ -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 = [];
......
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: false, selectable: true,
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
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: false, selectable: true,
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
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