Commit cbdf4e4b by DenSakh

Merge branch '2694_iframe_remove_button' into tiptap

# Conflicts:
#	src/QEditor.jsx
parents ce97b7dc a5f0c2db
/* eslint-disable no-undef */
/* eslint-disable no-case-declarations */
import React, { Fragment, useEffect, useState, useRef } from 'react' import React, { Fragment, useEffect, useState, useRef } from 'react'
import './index.scss' import './index.scss'
// import EditorModal from "./components/EditorModal" // import EditorModal from "./components/EditorModal"
...@@ -13,54 +15,72 @@ import TableHeader from '@tiptap/extension-table-header' ...@@ -13,54 +15,72 @@ import TableHeader from '@tiptap/extension-table-header'
import Focus from '@tiptap/extension-focus' import Focus from '@tiptap/extension-focus'
// import Link from '@tiptap/extension-link' // import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image' import Image from '@tiptap/extension-image'
import TextAlign from '@tiptap/extension-text-align'; import TextAlign from '@tiptap/extension-text-align'
import { Color } from '@tiptap/extension-color'; import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'; import Highlight from '@tiptap/extension-highlight'
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from '@tiptap/extension-text-style'
import Superscript from "@tiptap/extension-superscript"; import Superscript from '@tiptap/extension-superscript'
import Subscript from "@tiptap/extension-subscript"; import Subscript from '@tiptap/extension-subscript'
import ToolBar from "./components/ToolBar" import ToolBar from './components/ToolBar'
import EditorModal from "./components/EditorModal" import EditorModal from './components/EditorModal'
import Uploader from "./components/Uploader" import Uploader from './components/Uploader'
import Video from './extensions/Video' import Video from './extensions/Video'
import Iframe from './extensions/Iframe' import Iframe from './extensions/Iframe'
import CustomLink from './extensions/CustomLink' import CustomLink from './extensions/CustomLink'
import DragAndDrop from "./extensions/DragAndDrop"; import DragAndDrop from './extensions/DragAndDrop'
import { useReactMediaRecorder } from "react-media-recorder"; import { useReactMediaRecorder } from 'react-media-recorder'
import axios from "axios"; import axios from 'axios'
import ReactStopwatch from 'react-stopwatch'; import ReactStopwatch from 'react-stopwatch'
import Audio from "./extensions/Audio"; import Audio from './extensions/Audio'
import { isMobile } from 'react-device-detect'; import IframeModal from './modals/IframeModal'
import IframeCustomModal from './modals/IframeCustomModal'
import { isMobile } from 'react-device-detect'
const initialBubbleItems = ['bold', 'italic', 'underline', 'strike', 'superscript', 'subscript', '|', 'colorText', 'highlight']; const initialBubbleItems = [
'bold',
'italic',
'underline',
'strike',
'superscript',
'subscript',
'|',
'colorText',
'highlight'
]
const QEditor = ({ const QEditor = ({
value, value,
onChange = () => {}, onChange = () => {},
style, style,
uploadOptions = {url: "", errorMessage: ""}, uploadOptions = { url: '', errorMessage: '' },
toolsOptions = {type: 'all'} toolsOptions = { type: 'all' }
}) => { }) => {
global.uploadUrl = uploadOptions.url; global.uploadUrl = uploadOptions.url
const [innerModalType, setInnerModalType] = useState(null); const [innerModalType, setInnerModalType] = useState(null)
const [embedContent, setEmbedContent] = useState(''); const [embedContent, setEmbedContent] = useState('')
const [uploaderUid, setUploaderUid] = useState('uid' + new Date()); const [uploaderUid, setUploaderUid] = useState('uid' + new Date())
const [uploadedPaths, setUploadedPaths] = useState([]); const [uploadedPaths, setUploadedPaths] = useState([])
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false)
const [modalTitle, setModalTitle] = useState(''); const [modalTitle, setModalTitle] = useState('')
const [bubbleItems, setBubbleItems] = useState(initialBubbleItems); const [bubbleItems, setBubbleItems] = useState(initialBubbleItems)
const [colorsSelected, setColorsSelected] = useState(null); const [colorsSelected, setColorsSelected] = useState(null)
const [focusFromTo, setFocusFromTo] = useState(null); const [focusFromTo, setFocusFromTo] = useState(null)
const [oldFocusFromTo, setOldFocusFromTo] = useState(null); const [oldFocusFromTo, setOldFocusFromTo] = useState(null)
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false)
const [recordType, setRecordType] = useState({video: true}) const [recordType, setRecordType] = useState({ video: true })
// eslint-disable-next-line no-unused-vars
const getRgb = (hex) => { const getRgb = (hex) => {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})` : null; return result
? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
result[3],
16
)})`
: null
} }
const { const {
status, status,
...@@ -72,27 +92,27 @@ const QEditor = ({ ...@@ -72,27 +92,27 @@ const QEditor = ({
unMuteAudio, unMuteAudio,
isAudioMuted, isAudioMuted,
clearBlobUrl clearBlobUrl
} = useReactMediaRecorder(recordType); } = useReactMediaRecorder(recordType)
const videoRef = useRef(null); const videoRef = useRef(null)
useEffect(() => { useEffect(() => {
if (videoRef.current && previewStream) { if (videoRef.current && previewStream) {
videoRef.current.srcObject = previewStream; videoRef.current.srcObject = previewStream
} }
}, [previewStream]); }, [previewStream])
useEffect(() => { useEffect(() => {
if (focusFromTo !== oldFocusFromTo) { if (focusFromTo !== oldFocusFromTo) {
setColorsSelected(null) setColorsSelected(null)
setOldFocusFromTo(focusFromTo); setOldFocusFromTo(focusFromTo)
} }
}, [focusFromTo]) }, [focusFromTo])
const modalOpener = (type, title) => { const modalOpener = (type, title) => {
setModalTitle(title); setModalTitle(title)
setInnerModalType(type); setInnerModalType(type)
setModalIsOpen(true); setModalIsOpen(true)
} }
const colors = { const colors = {
color: [ color: [
...@@ -125,14 +145,14 @@ const QEditor = ({ ...@@ -125,14 +145,14 @@ const QEditor = ({
'#dea75b', '#dea75b',
'#ffe672' '#ffe672'
] ]
}; }
const toolsLib = { const toolsLib = {
link: { link: {
title: 'Вставить ссылку', title: 'Вставить ссылку',
onClick: () => { onClick: () => {
const previousUrl = editor.getAttributes('link').href const previousUrl = editor.getAttributes('link').href
const url = window.prompt('Введите URL', previousUrl); const url = window.prompt('Введите URL', previousUrl)
// cancelled // cancelled
if (url === null) { if (url === null) {
...@@ -141,13 +161,17 @@ const QEditor = ({ ...@@ -141,13 +161,17 @@ const QEditor = ({
// empty // empty
if (url === '') { if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run(); editor.chain().focus().extendMarkRange('link').unsetLink().run()
return return
} }
// update link // update link
editor.chain().focus().extendMarkRange('link').setLink({href: url, target: '_blank'}).run(); editor
.chain()
.focus()
.extendMarkRange('link')
.setLink({ href: url, target: '_blank' })
.run()
} }
}, },
file: { file: {
...@@ -170,29 +194,29 @@ const QEditor = ({ ...@@ -170,29 +194,29 @@ const QEditor = ({
title: 'Вставить презентацию pptx', title: 'Вставить презентацию pptx',
onClick: () => modalOpener('iframe_pptx', 'Вставить презентацию pptx') onClick: () => modalOpener('iframe_pptx', 'Вставить презентацию pptx')
}, },
audio: {
title: 'Вставить аудио файл',
onClick: () => modalOpener('audio', 'Вставить аудио файл')
},
iframe_pdf: { iframe_pdf: {
title: 'Вставить презентацию pdf', title: 'Вставить презентацию pdf',
onClick: () => modalOpener('iframe_pdf', 'Вставить презентацию pdf') onClick: () => modalOpener('iframe_pdf', 'Вставить презентацию pdf')
}, },
audio: {
title: 'Вставить аудио файл',
onClick: () => modalOpener('audio', 'Вставить аудио файл')
},
image: { image: {
title: 'Загрузить изображение', title: 'Загрузить изображение',
onClick: () => modalOpener('image', 'Загрузить изображение') onClick: () => modalOpener('image', 'Загрузить изображение')
}, },
h2: { h2: {
title: 'Заголовок 2', title: 'Заголовок 2',
onClick: () => editor.chain().focus().toggleHeading({level: 2}).run() onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run()
}, },
h3: { h3: {
title: 'Заголовок 3', title: 'Заголовок 3',
onClick: () => editor.chain().focus().toggleHeading({level: 3}).run() onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run()
}, },
h4: { h4: {
title: 'Заголовок 4', title: 'Заголовок 4',
onClick: () => editor.chain().focus().toggleHeading({level: 4}).run() onClick: () => editor.chain().focus().toggleHeading({ level: 4 }).run()
}, },
paragraph: { paragraph: {
title: 'Обычный', title: 'Обычный',
...@@ -261,27 +285,28 @@ const QEditor = ({ ...@@ -261,27 +285,28 @@ const QEditor = ({
alignLeft: { alignLeft: {
title: 'По левому краю', title: 'По левому краю',
onClick: () => { onClick: () => {
editor.commands.setTextAlign('left'); editor.commands.setTextAlign('left')
editor.chain().focus(); editor.chain().focus()
} }
}, },
alignCenter: { alignCenter: {
title: 'По центру', title: 'По центру',
onClick: () => { onClick: () => {
editor.commands.setTextAlign('center') editor.commands.setTextAlign('center')
editor.chain().focus(); editor.chain().focus()
} }
}, },
alignRight: { alignRight: {
title: 'По правому краю', title: 'По правому краю',
onClick: () => { onClick: () => {
editor.commands.setTextAlign('right'); editor.commands.setTextAlign('right')
editor.chain().focus(); editor.chain().focus()
} }
}, },
insertTable: { insertTable: {
title: 'Вставить таблицу', title: 'Вставить таблицу',
onClick: () => editor.chain().focus().insertTable({rows: 2, cols: 2}).run() onClick: () =>
editor.chain().focus().insertTable({ rows: 2, cols: 2 }).run()
}, },
deleteTable: { deleteTable: {
title: 'Удалить таблицу', title: 'Удалить таблицу',
...@@ -323,7 +348,7 @@ const QEditor = ({ ...@@ -323,7 +348,7 @@ const QEditor = ({
title: 'Цвет текста', title: 'Цвет текста',
onClick: () => { onClick: () => {
setColorsSelected('color') setColorsSelected('color')
editor.chain().focus(); editor.chain().focus()
} }
}, },
highlight: { highlight: {
...@@ -333,7 +358,7 @@ const QEditor = ({ ...@@ -333,7 +358,7 @@ const QEditor = ({
voicemessage: { voicemessage: {
title: 'Записать голосовое сообщение', title: 'Записать голосовое сообщение',
onClick: () => { onClick: () => {
setRecordType({audio: true}) setRecordType({ audio: true })
clearBlobUrl() clearBlobUrl()
modalOpener('voicemessage', 'Записать голосовое сообщение') modalOpener('voicemessage', 'Записать голосовое сообщение')
} }
...@@ -341,7 +366,7 @@ const QEditor = ({ ...@@ -341,7 +366,7 @@ const QEditor = ({
webcamera: { webcamera: {
title: 'Записать с камеры', title: 'Записать с камеры',
onClick: () => { onClick: () => {
setRecordType({video: true}) setRecordType({ video: true })
clearBlobUrl() clearBlobUrl()
modalOpener('webcamera', 'Записать с камеры') modalOpener('webcamera', 'Записать с камеры')
} }
...@@ -350,14 +375,14 @@ const QEditor = ({ ...@@ -350,14 +375,14 @@ const QEditor = ({
title: 'Записать экран', title: 'Записать экран',
onClick: () => { onClick: () => {
if (isMobile) { if (isMobile) {
setRecordType({video: true}) setRecordType({ video: true })
} else { } else {
setRecordType({screen: true}) setRecordType({ screen: true })
} }
clearBlobUrl() clearBlobUrl()
modalOpener('screencust', 'Записать экран') modalOpener('screencust', 'Записать экран')
} }
}, }
// katex: { // katex: {
// title: 'Вставить формулу', // title: 'Вставить формулу',
// onClick: () => { // onClick: () => {
...@@ -394,11 +419,11 @@ const QEditor = ({ ...@@ -394,11 +419,11 @@ const QEditor = ({
TextAlign.configure({ TextAlign.configure({
defaultAlignment: 'left', defaultAlignment: 'left',
types: ['heading', 'paragraph'], types: ['heading', 'paragraph'],
alignments: ['left', 'center', 'right', 'justify'], alignments: ['left', 'center', 'right', 'justify']
}), }),
TextStyle, TextStyle,
Color.configure({ Color.configure({
types: ['textStyle'], types: ['textStyle']
}), }),
Highlight.configure({ Highlight.configure({
multicolor: true multicolor: true
...@@ -409,7 +434,7 @@ const QEditor = ({ ...@@ -409,7 +434,7 @@ const QEditor = ({
}), }),
Focus.configure({ Focus.configure({
className: 'atma-editor-focused', className: 'atma-editor-focused',
mode: "all" mode: 'all'
}), }),
DragAndDrop.configure({ DragAndDrop.configure({
linkUpload: uploadOptions.url linkUpload: uploadOptions.url
...@@ -419,265 +444,295 @@ const QEditor = ({ ...@@ -419,265 +444,295 @@ const QEditor = ({
Subscript Subscript
], ],
content: value, content: value,
onUpdate: ({editor}) => onChange(editor.getHTML()), onUpdate: ({ editor }) => onChange(editor.getHTML()),
onFocus: ({editor}) => { onFocus: ({ editor }) => {
let wrap = editor.options.element.closest('.atma-editor-wrap'); const wrap = editor.options.element.closest('.atma-editor-wrap')
wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function (s) { wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function (s) {
s.classList.remove('show'); s.classList.remove('show')
}); })
} }
}) })
const buildActionsModal = (buttons = []) => { const buildActionsModal = (buttons = []) => {
if (buttons.length === 0) { if (buttons.length === 0) {
return null; return null
} }
return ( return (
<div className={'atma-editor-modal-action'}> <div className='atma-editor-modal-action'>
{ {buttons.map((btn, i) => (
buttons.map((btn, i) => ( <button
<button disabled={btn.disabled} type={'button'} key={'mAction' + i} disabled={btn.disabled}
type='button'
key={'mAction' + i}
className={'atma-editor-btn' + btn.className} className={'atma-editor-btn' + btn.className}
onClick={btn.onClick}>{btn.title}</button> onClick={btn.onClick}
)) >
} {btn.title}
</button>
))}
</div> </div>
) )
} }
const getUploader = ({accept = '*', ...o}) => { const getUploader = ({ accept = '*', ...o }) => {
let url = uploadOptions.url, let url = uploadOptions.url
multiple = true; let multiple = true
if (o.afterParams && o.afterParams.length > 0) { if (o.afterParams && o.afterParams.length > 0) {
if (uploadOptions.url.indexOf('?') !== -1) { if (uploadOptions.url.indexOf('?') !== -1) {
url = uploadOptions.url + '&' + o.afterParams.join('&'); url = uploadOptions.url + '&' + o.afterParams.join('&')
} else { } else {
url = uploadOptions.url + '?' + o.afterParams.join('&'); url = uploadOptions.url + '?' + o.afterParams.join('&')
} }
} }
if (typeof o.multiple !== 'undefined') { if (typeof o.multiple !== 'undefined') {
multiple = o.multiple; multiple = o.multiple
} }
return <Uploader return (
<Uploader
key={uploaderUid} key={uploaderUid}
accept={accept} accept={accept}
action={url} action={url}
errorMessage={uploadOptions.errorMessage} errorMessage={uploadOptions.errorMessage}
onSuccess={(file) => { onSuccess={(file) => {
let _uploadedPaths = [...uploadedPaths]; const _uploadedPaths = [...uploadedPaths]
_uploadedPaths.push(file)
_uploadedPaths.push(file);
setUploadedPaths(_uploadedPaths) setUploadedPaths(_uploadedPaths)
}} }}
onDelete={(deleteFile) => { onDelete={(deleteFile) => {
let deleteIdx = null; let deleteIdx = null
let _uploadedPaths = [...uploadedPaths]; const _uploadedPaths = [...uploadedPaths]
_uploadedPaths.map((f, i) => { _uploadedPaths.map((f, i) => {
if (f.uid === deleteFile.uid) { if (f.uid === deleteFile.uid) {
deleteIdx = i; deleteIdx = i
} }
}); })
_uploadedPaths.splice(deleteIdx, 1); _uploadedPaths.splice(deleteIdx, 1)
setUploadedPaths(_uploadedPaths) setUploadedPaths(_uploadedPaths)
}} }}
multiple={multiple} multiple={multiple}
modalType={innerModalType} modalType={innerModalType}
/> />
)
} }
const saveScreenCust = async (fileBlob) => { const saveScreenCust = async (fileBlob) => {
if (fileBlob) { if (fileBlob) {
setIsUploading(true) setIsUploading(true)
let blobData = await fetch(fileBlob).then((res) => res.blob()); const blobData = await fetch(fileBlob).then((res) => res.blob())
const data = new FormData(); const data = new FormData()
let file = new File([blobData], "name." + (recordType?.audio ? "mp3" : "webm")); const file = new File(
data.append("file", file); [blobData],
'name.' + (recordType?.audio ? 'mp3' : 'webm')
)
data.append('file', file)
const headers = {'Content-Type': 'multipart/form-data'}; const headers = { 'Content-Type': 'multipart/form-data' }
return new Promise(function (resolve) { return new Promise(function (resolve) {
axios.post(uploadOptions.url, data, {headers: headers}).then(response => { axios
if (response.data.state === "success") { .post(uploadOptions.url, data, { headers: headers })
.then((response) => {
if (response.data.state === 'success') {
resolve(response.data) resolve(response.data)
} }
setIsUploading(false) setIsUploading(false)
});
}) })
})
}
} }
};
const getInnerModal = () => { const getInnerModal = () => {
switch (innerModalType) { switch (innerModalType) {
case 'iframe': case 'iframe':
return ( return (
<Fragment> <IframeModal
<input type="text" value={embedContent} placeholder={'https://'} embedContent={embedContent}
onInput={(e) => setEmbedContent(e.target.value) setEmbedContent={setEmbedContent}
}/> />
<ul className={'atma-editor-soc-video'}>
<li className={'youtube'}/>
<li className={'vimeo'}/>
{/* <li className={'vk'}/> */}
<li className={'ok'}/>
<li className={'rutube'}/>
</ul>
</Fragment>
) )
case 'iframe_custom': case 'iframe_custom':
return ( return (
<Fragment> <IframeCustomModal
<textarea style={{width: '100%', height: '100%'}} rows={18} value={embedContent} placeholder={'<iframe></iframe>'} embedContent={embedContent}
onInput={(e) => setEmbedContent(e.target.value)} setEmbedContent={setEmbedContent}
/> />
</Fragment>
) )
case 'iframe_pptx': case 'iframe_pptx':
return ( return (
<Fragment>{getUploader({accept: 'application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.slideshow, application/vnd.openxmlformats-officedocument.presentationml.presentation', afterParams: ['no_convert=1']})}</Fragment> <Fragment>
{getUploader({
accept:
'application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.slideshow, application/vnd.openxmlformats-officedocument.presentationml.presentation',
afterParams: ['no_convert=1']
})}
</Fragment>
) )
case 'audio': case 'audio':
return ( return (
<Fragment>{getUploader({accept: '.wav, .mp3, .ogg'})}</Fragment> <Fragment>{getUploader({ accept: '.wav, .mp3, .ogg' })}</Fragment>
) )
case 'iframe_pdf': case 'iframe_pdf':
return ( return (
<Fragment>{getUploader({accept: 'application/pdf', afterParams: ['no_convert=1']})}</Fragment> <Fragment>
{getUploader({
accept: 'application/pdf',
afterParams: ['no_convert=1']
})}
</Fragment>
) )
case 'video': case 'video':
return ( return <Fragment>{getUploader({ accept: 'video/*' })}</Fragment>
<Fragment>{getUploader({accept: 'video/*'})}</Fragment>
)
case 'image': case 'image':
return ( return <Fragment>{getUploader({ accept: 'image/*' })}</Fragment>
<Fragment>{getUploader({accept: 'image/*'})}</Fragment>
)
case 'file': case 'file':
return ( return (
<Fragment>{getUploader({accept: '*', afterParams: ['no_convert=1']})}</Fragment> <Fragment>
{getUploader({ accept: '*', afterParams: ['no_convert=1'] })}
</Fragment>
) )
case 'voicemessage': case 'voicemessage':
return ( return (
<>
<Fragment> <Fragment>
{ {isMobile && (
isMobile && <div className='webwrap'>
<> <div>
<div className={"webwrap"}> Аудиозапись с мобильного устройства недоступна, <br />{' '}
<div>Аудиозапись с мобильного устройства недоступна, <br/> запишите стандартными запишите стандартными функциями устройства и воспользуйтесь
функциями устройства и воспользуйтесь кнопкой «Прикрепить файл» кнопкой «Прикрепить файл»
</div> </div>
</div> </div>
</> )}
} {!isMobile && (
{ <div className='audio-player'>
! isMobile && <div className='audio-player-start audio-player-margin'>
<div className={"audio-player"}> {status === 'recording' && !mediaBlobUrl ? (
<div className={"audio-player-start audio-player-margin"}> <div
{ onClick={stopRecording}
status === 'recording' && ! mediaBlobUrl ? className='audio-player-center-recording'
<div onClick={stopRecording} />
className={"audio-player-center-recording"}/> : ) : (
<div onClick={startRecording} className={"audio-player-center-start"}/> <div
} onClick={startRecording}
className='audio-player-center-start'
/>
)}
</div> </div>
<div className={"audio-player-voice audio-player-margin"}/> <div className='audio-player-voice audio-player-margin' />
{ {status === 'recording' && !mediaBlobUrl ? (
status === 'recording' && ! mediaBlobUrl ?
<ReactStopwatch <ReactStopwatch
seconds={0} seconds={0}
minutes={0} minutes={0}
hours={0} hours={0}
render={({formatted}) => { render={({ formatted }) => {
return ( return (
<span <span className='audio-player-timer audio-player-margin'>
className={"audio-player-timer audio-player-margin"}>{formatted}</span> {formatted}
</span>
) )
}} }}
/> : <span className={"audio-player-timer audio-player-margin"}/> />
} ) : (
<span className='audio-player-timer audio-player-margin' />
)}
</div> </div>
} )}
</Fragment> </Fragment>
</>
) )
case 'screencust': case 'screencust':
return ( return (
<> <>
<Fragment> <Fragment>
{ {isMobile && (
isMobile && <div className='webwrap'>
<> <div>
<div className={"webwrap"}> Запись экрана с мобильного устройства недоступна, <br />
<div>Запись экрана с мобильного устройства недоступна, <br/> запишите запишите стандартными функциями устройства и воспользуйтесь
стандартными функциями устройства и воспользуйтесь кнопкой «Загрузить видео» кнопкой «Загрузить видео»
</div> </div>
</div> </div>
</> )}
} {!isMobile && (
{
! isMobile &&
<> <>
<div className={"webwrap"}> <div className='webwrap'>
<div className={"webwrap-content"}> <div className='webwrap-content'>
{ {mediaBlobUrl ? (
mediaBlobUrl ? <video
<video className={"webwrap-video"} id={"id-video"} className='webwrap-video'
src={mediaBlobUrl} controls/> : status === "recording" && id='id-video'
<video className={"webwrap-video"} ref={videoRef} src={mediaBlobUrl}
src={previewStream} autoPlay controls={false}/> controls
} />
{ ) : (
status === 'recording' && ! mediaBlobUrl ? status === 'recording' && (
<video
className='webwrap-video'
ref={videoRef}
src={previewStream}
autoPlay
controls={false}
/>
)
)}
{status === 'recording' && !mediaBlobUrl ? (
<ReactStopwatch <ReactStopwatch
seconds={0} seconds={0}
minutes={0} minutes={0}
hours={0} hours={0}
render={({formatted}) => { render={({ formatted }) => {
return ( return (
<span className={"webwrap-timer"}> <span className='webwrap-timer'>{formatted}</span>
{formatted}
</span>
) )
}} }}
/> : <span className={"webwrap-timer"}>00:00:00</span> />
} ) : (
{ <span className='webwrap-timer'>00:00:00</span>
! mediaBlobUrl && )}
<div className={"webwrap-start-border"}> {!mediaBlobUrl && (
<div className='webwrap-start-border'>
<button <button
onClick={status === 'recording' ? stopRecording : startRecording} onClick={
className={status === 'recording' ? "webwrap-record-center" : "webwrap-start-center"}/> status === 'recording'
</div> ? stopRecording
: startRecording
} }
className={
status === 'recording'
? 'webwrap-record-center'
: 'webwrap-start-center'
}
/>
</div> </div>
)}
</div> </div>
<div className={"web-bottom-elements"}>
{mediaBlobUrl &&
<div onClick={clearBlobUrl} className={"web-button-wrap"}>
<div className={"web-button-rerecord"}/>
<span className={"web-button-rerecord-text"}>Перезаписать</span>
</div> </div>
<div className='web-bottom-elements'>
{mediaBlobUrl && (
<div onClick={clearBlobUrl} className='web-button-wrap'>
<div className='web-button-rerecord' />
<span className='web-button-rerecord-text'>
Перезаписать
</span>
</div>
)}
{!mediaBlobUrl && <div className='web-button-spacer' />}
{!mediaBlobUrl && (
<div
onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={
isAudioMuted ? 'web-button-unmute' : 'web-button-mute'
} }
{ />
! mediaBlobUrl && )}
<div className={"web-button-spacer"}/> <div className='web-button-spacer' />
}
{
! mediaBlobUrl &&
<div onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={isAudioMuted ? "web-button-unmute" : "web-button-mute"}/>
}
<div className={"web-button-spacer"}/>
</div> </div>
</> </>
} )}
</Fragment> </Fragment>
</> </>
) )
...@@ -685,75 +740,91 @@ const QEditor = ({ ...@@ -685,75 +740,91 @@ const QEditor = ({
return ( return (
<> <>
<Fragment> <Fragment>
{ {isMobile && (
isMobile && <div className='webwrap'>
<> <div>
<div className={"webwrap"}> Видеозапись с мобильного устройства недоступна, <br />
<div>Видеозапись с мобильного устройства недоступна, <br/> запишите стандартными запишите стандартными функциями устройства и воспользуйтесь
функциями устройства и воспользуйтесь кнопкой «Загрузить видео» кнопкой «Загрузить видео»
</div> </div>
</div> </div>
</> )}
} {!isMobile && (
{
! isMobile &&
<> <>
<div className={"webwrap"}> <div className='webwrap'>
<div className={"webwrap-content"}> <div className='webwrap-content'>
{ {mediaBlobUrl ? (
mediaBlobUrl ? <video
<video className={"webwrap-video"} id={"id-video"} className='webwrap-video'
id='id-video'
src={mediaBlobUrl} src={mediaBlobUrl}
controls/> : status === "recording" && controls
<video className={"webwrap-video"} ref={videoRef} />
) : (
status === 'recording' && (
<video
className='webwrap-video'
ref={videoRef}
src={previewStream} src={previewStream}
autoPlay controls={false}/> autoPlay
} controls={false}
{ />
status === 'recording' && ! mediaBlobUrl ? )
)}
{status === 'recording' && !mediaBlobUrl ? (
<ReactStopwatch <ReactStopwatch
seconds={0} seconds={0}
minutes={0} minutes={0}
hours={0} hours={0}
render={({formatted}) => { render={({ formatted }) => {
return ( return (
<span className={"webwrap-timer"}> <span className='webwrap-timer'>{formatted}</span>
{formatted}
</span>
) )
}} }}
/> : <span className={"webwrap-timer"}>00:00:00</span> />
} ) : (
{ <span className='webwrap-timer'>00:00:00</span>
! mediaBlobUrl && )}
<div className={"webwrap-start-border"}> {!mediaBlobUrl && (
<div className='webwrap-start-border'>
<button <button
onClick={status === 'recording' ? stopRecording : startRecording} onClick={
className={status === 'recording' ? "webwrap-record-center" : "webwrap-start-center"}/> status === 'recording'
</div> ? stopRecording
: startRecording
} }
className={
status === 'recording'
? 'webwrap-record-center'
: 'webwrap-start-center'
}
/>
</div> </div>
)}
</div> </div>
<div className={"web-bottom-elements"}>
{mediaBlobUrl &&
<div onClick={clearBlobUrl} className={"web-button-wrap"}>
<div className={"web-button-rerecord"}/>
<span className={"web-button-rerecord-text"}>Перезаписать</span>
</div> </div>
<div className='web-bottom-elements'>
{mediaBlobUrl && (
<div onClick={clearBlobUrl} className='web-button-wrap'>
<div className='web-button-rerecord' />
<span className='web-button-rerecord-text'>
Перезаписать
</span>
</div>
)}
{!mediaBlobUrl && <div className='web-button-spacer' />}
{!mediaBlobUrl && (
<div
onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={
isAudioMuted ? 'web-button-unmute' : 'web-button-mute'
} }
{ />
! mediaBlobUrl && )}
<div className={"web-button-spacer"}/> <div className='web-button-spacer' />
}
{
! mediaBlobUrl &&
<div onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={isAudioMuted ? "web-button-unmute" : "web-button-mute"}/>
}
<div className={"web-button-spacer"}/>
</div> </div>
</> </>
} )}
</Fragment> </Fragment>
</> </>
) )
...@@ -763,33 +834,33 @@ const QEditor = ({ ...@@ -763,33 +834,33 @@ const QEditor = ({
} }
const isDisabledAction = () => { const isDisabledAction = () => {
let isDisabled = false; let isDisabled = false
switch (innerModalType) { switch (innerModalType) {
case 'video': case 'video':
case 'image': case 'image':
if (uploadOptions.url === null || uploadedPaths.length === 0) { if (uploadOptions.url === null || uploadedPaths.length === 0) {
isDisabled = true; isDisabled = true
} }
break; break
case 'screencust': case 'screencust':
if (status === 'recording' || isUploading || ! mediaBlobUrl) { if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true; isDisabled = true
} }
break; break
case 'voicemessage': case 'voicemessage':
if (status === 'recording' || isUploading || ! mediaBlobUrl) { if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true; isDisabled = true
} }
break; break
case 'webcamera': case 'webcamera':
if (status === 'recording' || isUploading || ! mediaBlobUrl) { if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true; isDisabled = true
} }
break; break
case 'iframe': case 'iframe':
try { try {
let url = new URL(embedContent); const url = new URL(embedContent)
switch (url.hostname) { switch (url.hostname) {
case 'rutube.ru': case 'rutube.ru':
...@@ -800,131 +871,93 @@ const QEditor = ({ ...@@ -800,131 +871,93 @@ const QEditor = ({
case 'youtu.be': case 'youtu.be':
case 'youtube.com': case 'youtube.com':
case 'www.youtube.com': case 'www.youtube.com':
break; break
default: default:
isDisabled = true; isDisabled = true
} }
} catch (error) { } catch (error) {
isDisabled = true; isDisabled = true
} }
break; break
case 'iframe_custom': case 'iframe_custom':
let regex = new RegExp('(?:<iframe[^>]*)(?:(?:\\/>)|(?:>.*?<\\/iframe>))'); const regex = new RegExp(
isDisabled = !regex.test(embedContent); '(?:<iframe[^>]*)(?:(?:\\/>)|(?:>.*?<\\/iframe>))'
break; )
isDisabled = !regex.test(embedContent)
break
} }
return isDisabled; return isDisabled
} }
if ( ! editor) { if (!editor) {
return null return null
} }
return ( const buttons =
<div innerModalType === 'remove_iframe'
className="atma-editor-wrap" ? [
style={style}
>
<div className="atma-editor">
<ToolBar
editor={editor}
{...{toolsOptions}}
{...{toolsLib}}
/>
<BubbleMenu typpyOptions={{followCursor: true,}} editor={editor} shouldShow={({...o}) => {
let items = [];
if (o.from !== o.to && editor.isActive('paragraph') && editor.isActive('image') === false && document.querySelectorAll('.selectedCell').length === 0) {
items = initialBubbleItems;
}
if (editor.isActive('image') === true) {
items = ['alignLeft', 'alignCenter', 'alignRight'];
}
setFocusFromTo([o.from, o.to].join(':'));
if (items.length > 0) {
setBubbleItems(items);
return true;
}
}} tippyOptions={{duration: 100}}>
<div className={"atma-editor-bubble"} onClick={e => e.stopPropagation()}>
{ {
colorsSelected !== null ? title: 'Отмена',
colors[colorsSelected].map((itemColor, i) => { className: ' atma-editor-cancel',
return (<div key={'colors' + colorsSelected + i} onClick: () => {
className={'qcolors' + (itemColor === 'none' ? ' unset' : '')} stopRecording()
style={{background: itemColor}} onClick={() => { unMuteAudio()
clearBlobUrl()
if (itemColor === 'none') { setUploaderUid(`uid${new Date()}`)
colorsSelected === 'color' ? setUploadedPaths([])
editor.chain().focus().unsetHighlight().unsetColor().run() : setModalIsOpen(false)
editor.chain().focus().unsetColor().unsetHighlight().run();
} else {
colorsSelected === 'color' ?
editor.chain().focus().unsetHighlight().setColor(itemColor).run() :
editor.chain().focus().unsetColor().toggleHighlight({color: itemColor}).run();
}
setColorsSelected(null);
}}/>)
}) : bubbleItems.map((type, i) => {
if (type === '|') {
return (<div key={'bubbleSeparator' + i} className={'qseparator'}/>)
} else {
return (
<div
key={'bubbleItems' + i}
className={'qicon q' + type + (editor.isActive(type) ? ' active' : '')}
title={toolsLib[type] ? toolsLib[type].title : ''}
onClick={toolsLib[type].onClick}
/>
)
}
})
} }
</div> },
</BubbleMenu>
<EditorContent
editor={editor}
className={'atma-editor-content'}
/>
</div>
<EditorModal
isOpen={modalIsOpen}
title={modalTitle}
>
{ {
getInnerModal() title: 'Удалить',
className: ' atma-editor-complete',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
} }
{ }
buildActionsModal([ ]
: [
{ {
title: 'Отмена', title: 'Отмена',
className: ' atma-editor-cancel', className: ' atma-editor-cancel',
onClick: () => { onClick: () => {
stopRecording(); stopRecording()
unMuteAudio(); unMuteAudio()
clearBlobUrl(); clearBlobUrl()
setUploaderUid(`uid${new Date()}`); setUploaderUid(`uid${new Date()}`)
setUploadedPaths([]); setUploadedPaths([])
setModalIsOpen(false); setModalIsOpen(false)
} }
}, },
{ {
title: (mediaBlobUrl && uploadedPaths.length === 0) ? (isUploading ? 'Сохранение...' : 'Вставить') : 'Вставить', title:
mediaBlobUrl && uploadedPaths.length === 0
? isUploading
? 'Сохранение...'
: 'Вставить'
: 'Вставить',
className: ' atma-editor-complete', className: ' atma-editor-complete',
onClick: async () => { onClick: async () => {
if (status === 'recording' || isUploading) {
if ((status === 'recording' || isUploading)) { return false
return false;
} else { } else {
if (document.querySelectorAll('.atma-editor-uploader-progress').length > 0) { if (
if ( ! confirm('Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?')) { document.querySelectorAll('.atma-editor-uploader-progress')
return false; .length > 0
) {
if (
// eslint-disable-next-line no-undef
!confirm(
'Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?'
)
) {
return false
} }
} }
...@@ -932,135 +965,287 @@ const QEditor = ({ ...@@ -932,135 +965,287 @@ const QEditor = ({
switch (innerModalType) { switch (innerModalType) {
case 'image': case 'image':
uploadedPaths.map((file, i) => { uploadedPaths.map((file, i) => {
editor.chain().focus().setImage({src: file.path}).run(); editor.chain().focus().setImage({ src: file.path })
}); })
break break
case 'video': case 'video':
uploadedPaths.map((file, i) => { uploadedPaths.map((file, i) => {
editor.chain().focus().setVideo({ editor
.chain()
.focus()
.setVideo({
src: file.path, src: file.path,
poster: file.path + '.jpg' poster: file.path + '.jpg'
}).run(); })
}); .run()
})
break break
case 'voicemessage': case 'voicemessage':
if (mediaBlobUrl && uploadedPaths.length === 0) { if (mediaBlobUrl && uploadedPaths.length === 0) {
if ( ! isUploading) { if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then(data => { await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) { if (data?.file_path) {
editor.chain().focus().addVoiceMessage({src: data.file_path}).run(); editor
.chain()
.focus()
.addVoiceMessage({ src: data.file_path })
.run()
} }
}); })
} }
} }
break break
case 'screencust': case 'screencust':
if (mediaBlobUrl && uploadedPaths.length === 0) { if (mediaBlobUrl && uploadedPaths.length === 0) {
if ( ! isUploading) { if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then(data => { await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) { if (data?.file_path) {
editor.chain().focus().setVideo({src: data.file_path}).run(); editor
.chain()
.focus()
.setVideo({ src: data.file_path })
.run()
} }
}); })
} }
} }
break break
case 'webcamera': case 'webcamera':
if (mediaBlobUrl && uploadedPaths.length === 0) { if (mediaBlobUrl && uploadedPaths.length === 0) {
if ( ! isUploading) { if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then(data => { await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) { if (data?.file_path) {
editor.chain().focus().setVideo({src: data.file_path}).run(); editor
.chain()
.focus()
.setVideo({ src: data.file_path })
.run()
} }
}); })
} }
} }
break break
case 'iframe': case 'iframe':
let _url = embedContent; let _url = embedContent
let reg = /(http|https):\/\/([\w.]+\/?)\S*/; const reg = /(http|https):\/\/([\w.]+\/?)\S*/
const url = new URL(reg.test(_url) ? _url : 'https:' + _url); const url = new URL(
let urlId = url.pathname.replace(/\/$/ig, '').split('/').pop(); reg.test(_url) ? _url : 'https:' + _url
)
let urlId = url.pathname
.replace(/\/$/gi, '')
.split('/')
.pop()
switch (url.hostname) { switch (url.hostname) {
case 'rutube.ru': case 'rutube.ru':
case 'www.rutube.ru': case 'www.rutube.ru':
_url = `https://rutube.ru/pl/?pl_id&pl_type&pl_video=${urlId}`; _url = `https://rutube.ru/pl/?pl_id&pl_type&pl_video=${urlId}`
break break
case 'vimeo.com': case 'vimeo.com':
_url = `https://player.vimeo.com/video/${urlId}`; _url = `https://player.vimeo.com/video/${urlId}`
break break
case 'ok.ru': case 'ok.ru':
case 'www.ok.ru': case 'www.ok.ru':
_url = `//ok.ru/videoembed/${urlId}`; _url = `//ok.ru/videoembed/${urlId}`
break break
case 'youtu.be': case 'youtu.be':
case 'youtube.com': case 'youtube.com':
case 'www.youtube.com': case 'www.youtube.com':
if (url.hostname.indexOf('youtu.be') === -1 && url.search !== '') { if (
url.hostname.indexOf('youtu.be') === -1 &&
url.search !== ''
) {
if (url.searchParams.get('v')) { if (url.searchParams.get('v')) {
urlId = url.searchParams.get('v'); urlId = url.searchParams.get('v')
} }
} }
_url = `https://www.youtube.com/embed/${urlId}`; _url = `https://www.youtube.com/embed/${urlId}`
break break
} }
editor.chain().focus().setIframe({src: _url}).run(); editor.chain().focus().setIframe({ src: _url }).run()
break break
case 'iframe_custom': case 'iframe_custom':
editor.chain().focus().insertContent(embedContent).run(); editor.chain().focus().insertContent(embedContent).run()
break break
case 'iframe_pptx': case 'iframe_pptx':
uploadedPaths.map((file, i)=>{ uploadedPaths.map((file, i) => {
editor.chain().focus().insertContent(`<iframe src="https://view.officeapps.live.com/op/embed.aspx?src=${file.path}" width="100%" height="600px" frameBorder="0"></iframe>`).run(); editor
}) .chain()
break .focus()
case 'iframe_pdf': .insertContent(
uploadedPaths.map((file, i)=>{ `<iframe src="https://view.officeapps.live.com/op/embed.aspx?src=${file.path}" width="100%" height="600px" frameBorder="0"></iframe>`
editor.chain().focus().insertContent(`<iframe src="https://docs.google.com/viewer?embedded=true&url=${file.path}" width="100%" height="800px" frameBorder="0"></iframe>`).run(); )
.run()
}) })
break break
case 'audio': case 'audio':
uploadedPaths.map((file) => { uploadedPaths.map((file) => {
editor.chain().focus().insertContent(`<audio class="audio-player" controls="true" src="${file.path}" />`).run() editor
.chain()
.focus()
.insertContent(
`<audio class="audio-player" controls="true" src="${file.path}" />`
)
.run()
})
break
case 'iframe_pdf':
uploadedPaths.map((file, i) => {
editor
.chain()
.focus()
.insertContent(
`<iframe src="https://docs.google.com/viewer?embedded=true&url=${file.path}" width="100%" height="800px" frameBorder="0"></iframe>`
)
.run()
}) })
break; break
case 'file': case 'file':
uploadedPaths.map((file, i) => { uploadedPaths.map((file, i) => {
let exp = file.path.split('.'); let exp = file.path.split('.')
exp = exp[exp.length - 1] exp = exp[exp.length - 1]
editor.chain().focus().insertContent(`<a href="${file.path}" target="_blank" download="${file.name}.${exp}" data-size="${file.size}">${file.name}</a>`).run(); editor
}); .chain()
.focus()
.insertContent(
`
<a href="${file.path}" target="_blank" download="${file.name}.${exp}" data-size="${file.size}">${file.name}</a>
`
)
.run()
})
break break
} }
setModalIsOpen(false)
setModalIsOpen(false); clearBlobUrl()
clearBlobUrl(); setUploaderUid(`uid${new Date()}`)
setUploaderUid(`uid${new Date()}`); setEmbedContent('')
setEmbedContent(''); setUploadedPaths([])
setUploadedPaths([]); setModalTitle('')
setModalTitle('');
} catch (err) { } catch (err) {
console.log(err); console.log(err)
setModalIsOpen(false); setModalIsOpen(false)
clearBlobUrl(); clearBlobUrl()
setUploaderUid(`uid${new Date()}`); setUploaderUid(`uid${new Date()}`)
setEmbedContent(''); setEmbedContent('')
setUploadedPaths([]); setUploadedPaths([])
setModalTitle(''); setModalTitle('')
} }
} }
}, },
disabled: isDisabledAction() disabled: isDisabledAction()
} }
]) ]
return (
<div className='atma-editor-wrap' style={style}>
<div className='atma-editor'>
<ToolBar editor={editor} {...{ toolsOptions }} {...{ toolsLib }} />
<BubbleMenu
typpyOptions={{ followCursor: true }}
editor={editor}
shouldShow={({ ...o }) => {
let items = []
if (
o.from !== o.to &&
editor.isActive('paragraph') &&
editor.isActive('image') === false &&
document.querySelectorAll('.selectedCell').length === 0
) {
items = initialBubbleItems
} }
if (editor.isActive('image') === true) {
items = ['alignLeft', 'alignCenter', 'alignRight']
}
setFocusFromTo([o.from, o.to].join(':'))
if (items.length > 0) {
setBubbleItems(items)
return true
}
}}
tippyOptions={{ duration: 100 }}
>
<div
className='atma-editor-bubble'
onClick={(e) => e.stopPropagation()}
>
{colorsSelected !== null
? colors[colorsSelected].map((itemColor, i) => {
return (
<div
key={'colors' + colorsSelected + i}
className={
'qcolors' + (itemColor === 'none' ? ' unset' : '')
}
style={{ background: itemColor }}
onClick={() => {
if (itemColor === 'none') {
colorsSelected === 'color'
? editor
.chain()
.focus()
.unsetHighlight()
.unsetColor()
.run()
: editor
.chain()
.focus()
.unsetColor()
.unsetHighlight()
.run()
} else {
colorsSelected === 'color'
? editor
.chain()
.focus()
.unsetHighlight()
.setColor(itemColor)
.run()
: editor
.chain()
.focus()
.unsetColor()
.toggleHighlight({ color: itemColor })
.run()
}
setColorsSelected(null)
}}
/>
)
})
: bubbleItems.map((type, i) => {
if (type === '|') {
return (
<div key={'bubbleSeparator' + i} className='qseparator' />
)
} else {
return (
<div
key={'bubbleItems' + i}
className={
'qicon q' +
type +
(editor.isActive(type) ? ' active' : '')
}
title={toolsLib[type] ? toolsLib[type].title : ''}
onClick={toolsLib[type].onClick}
/>
)
}
})}
</div>
</BubbleMenu>
<EditorContent editor={editor} className='atma-editor-content' />
</div>
<EditorModal isOpen={modalIsOpen} title={modalTitle}>
{getInnerModal()}
{buildActionsModal(buttons)}
</EditorModal> </EditorModal>
</div> </div>
) )
} }
export default QEditor; export default QEditor
...@@ -9,68 +9,76 @@ const Iframe = Node.create({ ...@@ -9,68 +9,76 @@ const Iframe = Node.create({
addAttributes() { addAttributes() {
return { return {
"src": { src: {
default: null default: null
}, },
"frameborder": { frameborder: {
default: 0, default: 0
}, },
"allowfullscreen": { allowfullscreen: {
default: true, default: true,
parseHTML: () => { parseHTML: () => {
console.log(this) console.log(this)
}, }
}, }
} }
}, },
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'iframe', tag: 'iframe'
}, }
] ]
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return ['iframe', mergeAttributes(HTMLAttributes)]; return ['iframe', mergeAttributes(HTMLAttributes)]
}, },
addNodeView() { addNodeView() {
return ({ editor, node, ...a }) => { 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')
// div.className = 'aspect-w-16 aspect-h-9' + (editor.isEditable ? ' cursor-pointer' : ''); const closeBtn = document.createElement('button')
const iframe = document.createElement('iframe'); closeBtn.textContent = 'X'
if (editor.isEditable) { closeBtn.classList.add('closeBtn')
iframe.className = 'pointer-events-none'; closeBtn.addEventListener('click', function () {
} container.remove()
})
// if (editor.isEditable) {
// container.classList.add('pointer-events-none');
// }
container.append(closeBtn, iframe)
iframe.src = node.attrs.src;
iframe.frameBorder = node.attrs.frameborder;
iframe.allowfullscreen = node.attrs.allowfullscreen;
iframe.style = 'width:1280px;height:auto;aspect-ratio: 16 / 9;';
// div.append(video);
return { return {
dom: iframe, dom: container
} }
} }
}, },
addCommands() { addCommands() {
return { return {
setIframe: (options) => ({ tr, dispatch }) => { setIframe:
(options) =>
({ tr, dispatch }) => {
const { selection } = tr const { selection } = tr
const node = this.type.create(options) const node = this.type.create(options)
//
if (dispatch) { if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node) tr.replaceRangeWith(selection.from, selection.to, node)
} }
return true return true
},
} }
}, }
}); }
})
export default Iframe; export default Iframe
...@@ -1045,4 +1045,25 @@ body{ ...@@ -1045,4 +1045,25 @@ body{
.qseparator{ .qseparator{
width: 16px; width: 16px;
} }
.closeBtn {
position: relative;
display: flex;
justify-content: end;
border-radius: 50%;
border: none;
background-color: #2677e3;
color: #fff;
font-size: 0.5rem;
padding: 4px 6px;
top: 10px;
cursor: pointer;
right: 8px;
}
.customIframe {
width:1280px;
height:auto;
aspect-ratio: 16 / 9;
}
} }
import React, { Fragment } from 'react'
export default function IframeCustomModal({ embedContent, setEmbedContent }) {
return (
<Fragment>
<textarea
style={{ width: '100%', height: '100%' }}
rows={18}
value={embedContent}
placeholder='<iframe></iframe>'
onInput={(e) => setEmbedContent(e.target.value)}
/>
</Fragment>
)
}
import React, { Fragment } from 'react'
export default function IframeModal({ embedContent, setEmbedContent }) {
return (
<Fragment>
<input
type='text'
value={embedContent}
placeholder='https://'
onInput={(e) => setEmbedContent(e.target.value)}
/>
<ul className='atma-editor-soc-video'>
<li className='youtube' />
<li className='vimeo' />
{/* <li className={'vk'}/> */}
<li className='ok' />
<li className='rutube' />
</ul>
</Fragment>
)
}
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