Commit 934d6b2b by yakoff94

fix resizer

parent 70b17b67
...@@ -22,6 +22,7 @@ import TextStyle from '@tiptap/extension-text-style' ...@@ -22,6 +22,7 @@ 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 ImageResize from 'tiptap-imagresize'
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'
...@@ -37,1226 +38,1165 @@ import Audio from './extensions/Audio' ...@@ -37,1226 +38,1165 @@ import Audio from './extensions/Audio'
import IframeModal from './modals/IframeModal' import IframeModal from './modals/IframeModal'
import IframeCustomModal from './modals/IframeCustomModal' import IframeCustomModal from './modals/IframeCustomModal'
import { isMobile } from 'react-device-detect' import { isMobile } from 'react-device-detect'
import Resizable from './extensions/Resizing' import Resizable from './extensions/Resizing'
import Resizing from "./extensions/Resizing";
const initialBubbleItems = [ const initialBubbleItems = [
'bold', 'bold',
'italic', 'italic',
'underline', 'underline',
'strike', 'strike',
'superscript', 'superscript',
'subscript', 'subscript',
'|', '|',
'colorText', 'colorText',
'highlight' '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 // 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 return result
? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt( ? `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
result[3], result[3],
16 16
)})` )})`
: null : null
}
const {
status,
startRecording,
stopRecording,
mediaBlobUrl,
previewStream,
muteAudio,
unMuteAudio,
isAudioMuted,
clearBlobUrl
} = useReactMediaRecorder(recordType)
const videoRef = useRef(null)
useEffect(() => {
if (videoRef.current && previewStream) {
videoRef.current.srcObject = previewStream
}
}, [previewStream])
useEffect(() => {
if (focusFromTo !== oldFocusFromTo) {
setColorsSelected(null)
setOldFocusFromTo(focusFromTo)
} }
}, [focusFromTo]) const {
status,
startRecording,
stopRecording,
mediaBlobUrl,
previewStream,
muteAudio,
unMuteAudio,
isAudioMuted,
clearBlobUrl
} = useReactMediaRecorder(recordType)
const modalOpener = (type, title) => { const videoRef = useRef(null)
setModalTitle(title)
setInnerModalType(type)
setModalIsOpen(true)
}
const colors = {
color: [
'none',
'#8a8a8a',
'#afafaf',
'#44d724',
'#0bd9b2',
'#4fb7ff',
'#226aff',
'#b153e5',
'#f54f8e',
'#f34c37',
'#ee7027',
'#d27303',
'#ffd102'
],
highlight: [
'none',
'#9B9B9B',
'#CCCCCC',
'#9ee191',
'#43e7bf',
'#4fb7ff',
'#6d9ef5',
'#cd92e8',
'#f597bc',
'#fa9084',
'#ef9558',
'#dea75b',
'#ffe672'
]
}
const toolsLib = {
link: {
title: 'Вставить ссылку',
onClick: () => {
const previousUrl = editor.getAttributes('link').href
const url = window.prompt('Введите URL', previousUrl)
// cancelled useEffect(() => {
if (url === null) { if (videoRef.current && previewStream) {
return videoRef.current.srcObject = previewStream
} }
}, [previewStream])
// empty useEffect(() => {
if (url === '') { if (focusFromTo !== oldFocusFromTo) {
editor.chain().focus().extendMarkRange('link').unsetLink().run() setColorsSelected(null)
return setOldFocusFromTo(focusFromTo)
} }
}, [focusFromTo])
// update link const modalOpener = (type, title) => {
editor setModalTitle(title)
.chain() setInnerModalType(type)
.focus() setModalIsOpen(true)
.extendMarkRange('link') }
.setLink({ href: url, target: '_blank' }) const colors = {
.run() color: [
} 'none',
}, '#8a8a8a',
file: { '#afafaf',
title: 'Прикрепить файл', '#44d724',
onClick: () => modalOpener('file', 'Прикрепить файл') '#0bd9b2',
}, '#4fb7ff',
video: { '#226aff',
title: 'Загрузить видео', '#b153e5',
onClick: () => modalOpener('video', 'Загрузить видео') '#f54f8e',
}, '#f34c37',
iframe: { '#ee7027',
title: 'Видео по ссылке', '#d27303',
onClick: () => modalOpener('iframe', 'Видео по ссылке') '#ffd102'
}, ],
iframe_custom: { highlight: [
title: 'Вставить iframe', 'none',
onClick: () => modalOpener('iframe_custom', 'Вставить iframe') '#9B9B9B',
}, '#CCCCCC',
iframe_pptx: { '#9ee191',
title: 'Вставить презентацию pptx', '#43e7bf',
onClick: () => modalOpener('iframe_pptx', 'Вставить презентацию pptx') '#4fb7ff',
}, '#6d9ef5',
iframe_pdf: { '#cd92e8',
title: 'Вставить презентацию pdf', '#f597bc',
onClick: () => modalOpener('iframe_pdf', 'Вставить презентацию pdf') '#fa9084',
}, '#ef9558',
audio: { '#dea75b',
title: 'Вставить аудио файл', '#ffe672'
onClick: () => modalOpener('audio', 'Вставить аудио файл') ]
},
image: {
title: 'Загрузить изображение',
onClick: () => modalOpener('image', 'Загрузить изображение')
},
h2: {
title: 'Заголовок 2',
onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run()
},
h3: {
title: 'Заголовок 3',
onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run()
},
h4: {
title: 'Заголовок 4',
onClick: () => editor.chain().focus().toggleHeading({ level: 4 }).run()
},
paragraph: {
title: 'Обычный',
onClick: () => editor.chain().focus().setParagraph().run()
},
bold: {
title: 'Жирный',
onClick: () => editor.chain().focus().toggleBold().run()
},
italic: {
title: 'Курсив',
onClick: () => editor.chain().focus().toggleItalic().run()
},
underline: {
title: 'Подчеркнутый',
onClick: () => editor.chain().focus().toggleUnderline().run()
},
strike: {
title: 'Зачеркнутый',
onClick: () => editor.chain().focus().toggleStrike().run()
},
superscript: {
title: 'Надстрочный символ',
onClick: () => editor.chain().focus().toggleSuperscript().run()
},
subscript: {
title: 'Подстрочный символ',
onClick: () => editor.chain().focus().toggleSubscript().run()
},
codeBlock: {
title: 'Код',
onClick: () => editor.chain().focus().toggleCodeBlock().run()
},
clearMarks: {
title: 'Очистить форматирование',
onClick: () => editor.chain().focus().unsetAllMarks().run()
},
bulletList: {
title: 'Маркированный список',
onClick: () => editor.chain().focus().toggleBulletList().run()
},
orderedList: {
title: 'Нумированный список',
onClick: () => editor.chain().focus().toggleOrderedList().run()
},
blockquote: {
title: 'Цитата',
onClick: () => editor.chain().focus().toggleBlockquote().run()
},
hardBreak: {
title: 'Перенос строки',
onClick: () => editor.chain().focus().setHardBreak().run()
},
hr: {
title: 'Горизонтальная линия',
onClick: () => editor.chain().focus().setHorizontalRule().run()
},
undo: {
title: 'Действие назад',
onClick: () => editor.chain().focus().undo().run()
},
redo: {
title: 'Действие вперед',
onClick: () => editor.chain().focus().redo().run()
},
alignLeft: {
title: 'По левому краю',
onClick: () => {
editor.commands.setTextAlign('left')
editor.chain().focus()
}
},
alignCenter: {
title: 'По центру',
onClick: () => {
editor.commands.setTextAlign('center')
editor.chain().focus()
}
},
alignRight: {
title: 'По правому краю',
onClick: () => {
editor.commands.setTextAlign('right')
editor.chain().focus()
}
},
insertTable: {
title: 'Вставить таблицу',
onClick: () =>
editor.chain().focus().insertTable({ rows: 2, cols: 2 }).run()
},
deleteTable: {
title: 'Удалить таблицу',
onClick: () => editor.chain().focus().deleteTable().run()
},
addRowBefore: {
title: 'Вставить строку перед',
onClick: () => editor.chain().focus().addRowBefore().run()
},
addRowAfter: {
title: 'Вставить строку после',
onClick: () => editor.chain().focus().addRowAfter().run()
},
deleteRow: {
title: 'Удалить строку',
onClick: () => editor.chain().focus().deleteRow().run()
},
addColumnBefore: {
title: 'Вставить столбец перед',
onClick: () => editor.chain().focus().addColumnBefore().run()
},
addColumnAfter: {
title: 'Вставить столбец после',
onClick: () => editor.chain().focus().addColumnAfter().run()
},
deleteColumn: {
title: 'Удалить столбец',
onClick: () => editor.chain().focus().deleteColumn().run()
},
mergeOrSplit: {
title: 'Объединить/разъединить ячейки',
onClick: () => editor.chain().focus().mergeOrSplit().run()
},
toggleHeaderCell: {
title: 'Добавить/удалить заголовок',
onClick: () => editor.chain().focus().toggleHeaderCell().run()
},
colorText: {
title: 'Цвет текста',
onClick: () => {
setColorsSelected('color')
editor.chain().focus()
}
},
highlight: {
title: 'Цвет фона',
onClick: () => setColorsSelected('highlight')
},
voicemessage: {
title: 'Записать голосовое сообщение',
onClick: () => {
setRecordType({ audio: true })
clearBlobUrl()
modalOpener('voicemessage', 'Записать голосовое сообщение')
}
},
webcamera: {
title: 'Записать с камеры',
onClick: () => {
setRecordType({ video: true })
clearBlobUrl()
modalOpener('webcamera', 'Записать с камеры')
}
},
screencust: {
title: 'Записать экран',
onClick: () => {
if (isMobile) {
setRecordType({ video: true })
} else {
setRecordType({ screen: true })
}
clearBlobUrl()
modalOpener('screencust', 'Записать экран')
}
} }
// katex: {
// title: 'Вставить формулу',
// onClick: () => {
//
// console.log(katex.renderToString(String.raw`c = \pm\sqrt{a^2 + b^2}`));
//
// // editor.chain().focus().insertContent()
// }
// }
}
const editor = useEditor({ const toolsLib = {
extensions: [ link: {
StarterKit, title: 'Вставить ссылку',
Underline, onClick: () => {
Image.configure({ const previousUrl = editor.getAttributes('link').href
inline: true const url = window.prompt('Введите URL', previousUrl)
}),
Resizable.configure({ // cancelled
types: ["image"], // resizable type if (url === null) {
handlerStyle: { // handler point style return
width: "8px", }
height: "8px",
background: "#07c160", // empty
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor.chain().focus().extendMarkRange('link').setLink({href: url, target: '_blank'}).run()
}
}, },
layerStyle: { // layer mask style file: {
border: "2px solid #07c160", title: 'Прикрепить файл',
onClick: () => modalOpener('file', 'Прикрепить файл')
}, },
}), video: {
// Link.configure({ title: 'Загрузить видео',
// autolink: true, onClick: () => modalOpener('video', 'Загрузить видео')
// linkOnPaste: true, },
// validate: (href)=> console.log(href), iframe: {
// }), title: 'Видео по ссылке',
Video, onClick: () => modalOpener('iframe', 'Видео по ссылке')
Iframe, },
Table.configure({ iframe_custom: {
resizable: true, title: 'Вставить iframe',
allowTableNodeSelection: true onClick: () => modalOpener('iframe_custom', 'Вставить iframe')
}), },
TableRow, iframe_pptx: {
TableHeader, title: 'Вставить презентацию pptx',
TableCell, onClick: () => modalOpener('iframe_pptx', 'Вставить презентацию pptx')
BubbleMenu, },
TextAlign.configure({ iframe_pdf: {
defaultAlignment: 'left', title: 'Вставить презентацию pdf',
types: ['heading', 'paragraph'], onClick: () => modalOpener('iframe_pdf', 'Вставить презентацию pdf')
alignments: ['left', 'center', 'right', 'justify'] },
}), audio: {
TextStyle, title: 'Вставить аудио файл',
Color.configure({ onClick: () => modalOpener('audio', 'Вставить аудио файл')
types: ['textStyle'] },
}), image: {
Highlight.configure({ title: 'Загрузить изображение',
multicolor: true onClick: () => modalOpener('image', 'Загрузить изображение')
}), },
CustomLink.configure({ h2: {
linkOnPaste: false, title: 'Заголовок 2',
openOnClick: false onClick: () => editor.chain().focus().toggleHeading({level: 2}).run()
}), },
Focus.configure({ h3: {
className: 'atma-editor-focused', title: 'Заголовок 3',
mode: 'all' onClick: () => editor.chain().focus().toggleHeading({level: 3}).run()
}), },
DragAndDrop.configure({ h4: {
linkUpload: uploadOptions.url title: 'Заголовок 4',
}), onClick: () => editor.chain().focus().toggleHeading({level: 4}).run()
Audio, },
Superscript, paragraph: {
Subscript title: 'Обычный',
], onClick: () => editor.chain().focus().setParagraph().run()
content: value, },
onUpdate: ({ editor }) => onChange(editor.getHTML()), bold: {
onFocus: ({ editor }) => { title: 'Жирный',
const wrap = editor.options.element.closest('.atma-editor-wrap') onClick: () => editor.chain().focus().toggleBold().run()
},
wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function (s) { italic: {
s.classList.remove('show') title: 'Курсив',
}) onClick: () => editor.chain().focus().toggleItalic().run()
},
underline: {
title: 'Подчеркнутый',
onClick: () => editor.chain().focus().toggleUnderline().run()
},
strike: {
title: 'Зачеркнутый',
onClick: () => editor.chain().focus().toggleStrike().run()
},
superscript: {
title: 'Надстрочный символ',
onClick: () => editor.chain().focus().toggleSuperscript().run()
},
subscript: {
title: 'Подстрочный символ',
onClick: () => editor.chain().focus().toggleSubscript().run()
},
codeBlock: {
title: 'Код',
onClick: () => editor.chain().focus().toggleCodeBlock().run()
},
clearMarks: {
title: 'Очистить форматирование',
onClick: () => editor.chain().focus().unsetAllMarks().run()
},
bulletList: {
title: 'Маркированный список',
onClick: () => editor.chain().focus().toggleBulletList().run()
},
orderedList: {
title: 'Нумированный список',
onClick: () => editor.chain().focus().toggleOrderedList().run()
},
blockquote: {
title: 'Цитата',
onClick: () => editor.chain().focus().toggleBlockquote().run()
},
hardBreak: {
title: 'Перенос строки',
onClick: () => editor.chain().focus().setHardBreak().run()
},
hr: {
title: 'Горизонтальная линия',
onClick: () => editor.chain().focus().setHorizontalRule().run()
},
undo: {
title: 'Действие назад',
onClick: () => editor.chain().focus().undo().run()
},
redo: {
title: 'Действие вперед',
onClick: () => editor.chain().focus().redo().run()
},
alignLeft: {
title: 'По левому краю',
onClick: () => {
editor.commands.setTextAlign('left')
editor.chain().focus()
}
},
alignCenter: {
title: 'По центру',
onClick: () => {
editor.commands.setTextAlign('center');
editor.chain().focus();
}
},
alignRight: {
title: 'По правому краю',
onClick: () => {
editor.commands.setTextAlign('right')
editor.chain().focus()
}
},
insertTable: {
title: 'Вставить таблицу',
onClick: () =>
editor.chain().focus().insertTable({rows: 2, cols: 2}).run()
},
deleteTable: {
title: 'Удалить таблицу',
onClick: () => editor.chain().focus().deleteTable().run()
},
addRowBefore: {
title: 'Вставить строку перед',
onClick: () => editor.chain().focus().addRowBefore().run()
},
addRowAfter: {
title: 'Вставить строку после',
onClick: () => editor.chain().focus().addRowAfter().run()
},
deleteRow: {
title: 'Удалить строку',
onClick: () => editor.chain().focus().deleteRow().run()
},
addColumnBefore: {
title: 'Вставить столбец перед',
onClick: () => editor.chain().focus().addColumnBefore().run()
},
addColumnAfter: {
title: 'Вставить столбец после',
onClick: () => editor.chain().focus().addColumnAfter().run()
},
deleteColumn: {
title: 'Удалить столбец',
onClick: () => editor.chain().focus().deleteColumn().run()
},
mergeOrSplit: {
title: 'Объединить/разъединить ячейки',
onClick: () => editor.chain().focus().mergeOrSplit().run()
},
toggleHeaderCell: {
title: 'Добавить/удалить заголовок',
onClick: () => editor.chain().focus().toggleHeaderCell().run()
},
colorText: {
title: 'Цвет текста',
onClick: () => {
setColorsSelected('color')
editor.chain().focus()
}
},
highlight: {
title: 'Цвет фона',
onClick: () => setColorsSelected('highlight')
},
voicemessage: {
title: 'Записать голосовое сообщение',
onClick: () => {
setRecordType({audio: true})
clearBlobUrl()
modalOpener('voicemessage', 'Записать голосовое сообщение')
}
},
webcamera: {
title: 'Записать с камеры',
onClick: () => {
setRecordType({video: true})
clearBlobUrl()
modalOpener('webcamera', 'Записать с камеры')
}
},
screencust: {
title: 'Записать экран',
onClick: () => {
if (isMobile) {
setRecordType({video: true})
} else {
setRecordType({screen: true})
}
clearBlobUrl()
modalOpener('screencust', 'Записать экран')
}
}
// katex: {
// title: 'Вставить формулу',
// onClick: () => {
//
// console.log(katex.renderToString(String.raw`c = \pm\sqrt{a^2 + b^2}`));
//
// // editor.chain().focus().insertContent()
// }
// }
} }
})
const buildActionsModal = (buttons = []) => { const editor = useEditor({
if (buttons.length === 0) { extensions: [
return null StarterKit,
} Underline,
Image.configure({
inline: true
}),
// ImageResize.configure({resizeIcon: <>ResizeMe</>,
Resizable.configure({
types: ["image"], // resizable type
handlerStyle: { // handler point style
width: "8px",
height: "8px",
background: "#07c160",
},
layerStyle: { // layer mask style
border: "2px solid #07c160",
},
}),
// Link.configure({
// autolink: true,
// linkOnPaste: true,
// validate: (href)=> console.log(href),
// }),
Video,
Iframe,
Table.configure({
resizable: true,
allowTableNodeSelection: true
}),
TableRow,
TableHeader,
TableCell,
BubbleMenu,
TextAlign.configure({
defaultAlignment: 'left',
types: ['heading', 'paragraph'],
alignments: ['left', 'center', 'right', 'justify']
}),
TextStyle,
Color.configure({
types: ['textStyle']
}),
Highlight.configure({
multicolor: true
}),
CustomLink.configure({
linkOnPaste: false,
openOnClick: false
}),
Focus.configure({
className: 'atma-editor-focused',
mode: 'all'
}),
DragAndDrop.configure({
linkUpload: uploadOptions.url
}),
Audio,
Superscript,
Subscript
],
content: value,
onUpdate: ({editor}) => onChange(editor.getHTML()),
onFocus: ({editor}) => {
const wrap = editor.options.element.closest('.atma-editor-wrap')
return ( wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function (s) {
<div className='atma-editor-modal-action'> s.classList.remove('show')
{buttons.map((btn, i) => ( })
<button }
disabled={btn.disabled} })
type='button'
key={'mAction' + i}
className={'atma-editor-btn' + btn.className}
onClick={btn.onClick}
>
{btn.title}
</button>
))}
</div>
)
}
const getUploader = ({ accept = '*', ...o }) => { const buildActionsModal = (buttons = []) => {
let url = uploadOptions.url if (buttons.length === 0) {
let multiple = true return null
if (o.afterParams && o.afterParams.length > 0) { }
if (uploadOptions.url.indexOf('?') !== -1) {
url = uploadOptions.url + '&' + o.afterParams.join('&')
} else {
url = uploadOptions.url + '?' + o.afterParams.join('&')
}
}
if (typeof o.multiple !== 'undefined') { return (
multiple = o.multiple <div className='atma-editor-modal-action'>
{buttons.map((btn, i) => (
<button
disabled={btn.disabled}
type='button'
key={'mAction' + i}
className={'atma-editor-btn' + btn.className}
onClick={btn.onClick}
>
{btn.title}
</button>
))}
</div>
)
} }
return ( const getUploader = ({accept = '*', ...o}) => {
<Uploader let url = uploadOptions.url
key={uploaderUid} let multiple = true
accept={accept} if (o.afterParams && o.afterParams.length > 0) {
action={url} if (uploadOptions.url.indexOf('?') !== -1) {
errorMessage={uploadOptions.errorMessage} url = uploadOptions.url + '&' + o.afterParams.join('&')
onSuccess={(file) => { } else {
const _uploadedPaths = [...uploadedPaths] url = uploadOptions.url + '?' + o.afterParams.join('&')
_uploadedPaths.push(file)
setUploadedPaths(_uploadedPaths)
}}
onDelete={(deleteFile) => {
let deleteIdx = null
const _uploadedPaths = [...uploadedPaths]
_uploadedPaths.map((f, i) => {
if (f.uid === deleteFile.uid) {
deleteIdx = i
} }
}) }
_uploadedPaths.splice(deleteIdx, 1)
setUploadedPaths(_uploadedPaths)
}}
multiple={multiple}
modalType={innerModalType}
/>
)
}
const saveScreenCust = async (fileBlob) => { if (typeof o.multiple !== 'undefined') {
if (fileBlob) { multiple = o.multiple
setIsUploading(true) }
const blobData = await fetch(fileBlob).then((res) => res.blob())
const data = new FormData() return (
const file = new File( <Uploader
[blobData], key={uploaderUid}
'name.' + (recordType?.audio ? 'mp3' : 'webm') accept={accept}
) action={url}
data.append('file', file) errorMessage={uploadOptions.errorMessage}
onSuccess={(file) => {
const _uploadedPaths = [...uploadedPaths]
_uploadedPaths.push(file)
setUploadedPaths(_uploadedPaths)
}}
onDelete={(deleteFile) => {
let deleteIdx = null
const _uploadedPaths = [...uploadedPaths]
const headers = { 'Content-Type': 'multipart/form-data' } _uploadedPaths.map((f, i) => {
if (f.uid === deleteFile.uid) {
deleteIdx = i
}
})
_uploadedPaths.splice(deleteIdx, 1)
setUploadedPaths(_uploadedPaths)
}}
multiple={multiple}
modalType={innerModalType}
/>
)
}
return new Promise(function (resolve) { const saveScreenCust = async (fileBlob) => {
axios if (fileBlob) {
.post(uploadOptions.url, data, { headers: headers }) setIsUploading(true)
.then((response) => { const blobData = await fetch(fileBlob).then((res) => res.blob())
if (response.data.state === 'success') {
resolve(response.data) const data = new FormData()
} const file = new File(
setIsUploading(false) [blobData],
}) 'name.' + (recordType?.audio ? 'mp3' : 'webm')
}) )
data.append('file', file)
const headers = {'Content-Type': 'multipart/form-data'}
return new Promise(function (resolve) {
axios.post(uploadOptions.url, data, {headers: headers}).then((response) => {
if (response.data.state === 'success') {
resolve(response.data)
}
setIsUploading(false)
})
})
}
} }
}
const getInnerModal = () => { const getInnerModal = () => {
switch (innerModalType) { switch (innerModalType) {
case 'iframe': case 'iframe':
return ( return (
<IframeModal <IframeModal
embedContent={embedContent} embedContent={embedContent}
setEmbedContent={setEmbedContent} setEmbedContent={setEmbedContent}
/>
)
case 'iframe_custom':
return (
<IframeCustomModal
embedContent={embedContent}
setEmbedContent={setEmbedContent}
/>
)
case 'iframe_pptx':
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>
)
case 'audio':
return (
<Fragment>{getUploader({ accept: '.wav, .mp3, .ogg' })}</Fragment>
)
case 'iframe_pdf':
return (
<Fragment>
{getUploader({
accept: 'application/pdf',
afterParams: ['no_convert=1']
})}
</Fragment>
)
case 'video':
return <Fragment>{getUploader({ accept: 'video/*' })}</Fragment>
case 'image':
return <Fragment>{getUploader({ accept: 'image/*' })}</Fragment>
case 'file':
return (
<Fragment>
{getUploader({ accept: '*', afterParams: ['no_convert=1'] })}
</Fragment>
)
case 'voicemessage':
return (
<Fragment>
{isMobile && (
<div className='webwrap'>
<div>
Аудиозапись с мобильного устройства недоступна, <br />{' '}
запишите стандартными функциями устройства и воспользуйтесь
кнопкой «Прикрепить файл»
</div>
</div>
)}
{!isMobile && (
<div className='audio-player'>
<div className='audio-player-start audio-player-margin'>
{status === 'recording' && !mediaBlobUrl ? (
<div
onClick={stopRecording}
className='audio-player-center-recording'
/> />
) : ( )
<div case 'iframe_custom':
onClick={startRecording} return (
className='audio-player-center-start' <IframeCustomModal
embedContent={embedContent}
setEmbedContent={setEmbedContent}
/> />
)} )
</div> case 'iframe_pptx':
<div className='audio-player-voice audio-player-margin' /> return (
{status === 'recording' && !mediaBlobUrl ? ( <Fragment>
<ReactStopwatch {getUploader({
seconds={0} accept:
minutes={0} 'application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.slideshow, application/vnd.openxmlformats-officedocument.presentationml.presentation',
hours={0} afterParams: ['no_convert=1']
render={({ formatted }) => { })}
return ( </Fragment>
<span className='audio-player-timer audio-player-margin'> )
case 'audio':
return (
<Fragment>{getUploader({accept: '.wav, .mp3, .ogg'})}</Fragment>
)
case 'iframe_pdf':
return (
<Fragment>
{getUploader({
accept: 'application/pdf',
afterParams: ['no_convert=1']
})}
</Fragment>
)
case 'video':
return <Fragment>{getUploader({accept: 'video/*'})}</Fragment>
case 'image':
return <Fragment>{getUploader({accept: 'image/*'})}</Fragment>
case 'file':
return (
<Fragment>
{getUploader({accept: '*', afterParams: ['no_convert=1']})}
</Fragment>
)
case 'voicemessage':
return (
<Fragment>
{isMobile && (
<div className='webwrap'>
<div>
Аудиозапись с мобильного устройства недоступна, <br/>{' '}
запишите стандартными функциями устройства и воспользуйтесь
кнопкой «Прикрепить файл»
</div>
</div>
)}
{ ! isMobile && (
<div className='audio-player'>
<div className='audio-player-start audio-player-margin'>
{status === 'recording' && ! mediaBlobUrl ? (
<div
onClick={stopRecording}
className='audio-player-center-recording'
/>
) : (
<div
onClick={startRecording}
className='audio-player-center-start'
/>
)}
</div>
<div className='audio-player-voice audio-player-margin'/>
{status === 'recording' && ! mediaBlobUrl ? (
<ReactStopwatch
seconds={0}
minutes={0}
hours={0}
render={({formatted}) => {
return (
<span className='audio-player-timer audio-player-margin'>
{formatted} {formatted}
</span> </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 className='webwrap'>
<div> <div>
Запись экрана с мобильного устройства недоступна, <br /> Запись экрана с мобильного устройства недоступна, <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' className='webwrap-video'
id='id-video' id='id-video'
src={mediaBlobUrl} src={mediaBlobUrl}
controls controls
/> />
) : ( ) : (
status === 'recording' && ( status === 'recording' && (
<video <video
className='webwrap-video' className='webwrap-video'
ref={videoRef} ref={videoRef}
src={previewStream} src={previewStream}
autoPlay autoPlay
controls={false} 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'>{formatted}</span> <span className='webwrap-timer'>{formatted}</span>
) )
}} }}
/> />
) : ( ) : (
<span className='webwrap-timer'>00:00:00</span> <span className='webwrap-timer'>00:00:00</span>
)} )}
{!mediaBlobUrl && ( { ! mediaBlobUrl && (
<div className='webwrap-start-border'> <div className='webwrap-start-border'>
<button <button
onClick={ onClick={
status === 'recording' status === 'recording'
? stopRecording ? stopRecording
: startRecording : startRecording
} }
className={ className={
status === 'recording' status === 'recording'
? 'webwrap-record-center' ? 'webwrap-record-center'
: 'webwrap-start-center' : 'webwrap-start-center'
} }
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className='web-bottom-elements'> <div className='web-bottom-elements'>
{mediaBlobUrl && ( {mediaBlobUrl && (
<div onClick={clearBlobUrl} className='web-button-wrap'> <div onClick={clearBlobUrl} className='web-button-wrap'>
<div className='web-button-rerecord' /> <div className='web-button-rerecord'/>
<span className='web-button-rerecord-text'> <span className='web-button-rerecord-text'>
Перезаписать Перезаписать
</span> </span>
</div> </div>
)} )}
{!mediaBlobUrl && <div className='web-button-spacer' />} { ! mediaBlobUrl && <div className='web-button-spacer'/>}
{!mediaBlobUrl && ( { ! mediaBlobUrl && (
<div <div
onClick={isAudioMuted ? unMuteAudio : muteAudio} onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={ className={
isAudioMuted ? 'web-button-unmute' : 'web-button-mute' isAudioMuted ? 'web-button-unmute' : 'web-button-mute'
} }
/> />
)} )}
<div className='web-button-spacer' /> <div className='web-button-spacer'/>
</div> </div>
</> </>
)} )}
</Fragment> </Fragment>
</> </>
) )
case 'webcamera': case 'webcamera':
return ( return (
<> <>
<Fragment> <Fragment>
{isMobile && ( {isMobile && (
<div className='webwrap'> <div className='webwrap'>
<div> <div>
Видеозапись с мобильного устройства недоступна, <br /> Видеозапись с мобильного устройства недоступна, <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' className='webwrap-video'
id='id-video' id='id-video'
src={mediaBlobUrl} src={mediaBlobUrl}
controls controls
/> />
) : ( ) : (
status === 'recording' && ( status === 'recording' && (
<video <video
className='webwrap-video' className='webwrap-video'
ref={videoRef} ref={videoRef}
src={previewStream} src={previewStream}
autoPlay autoPlay
controls={false} 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'>{formatted}</span> <span className='webwrap-timer'>{formatted}</span>
) )
}} }}
/> />
) : ( ) : (
<span className='webwrap-timer'>00:00:00</span> <span className='webwrap-timer'>00:00:00</span>
)} )}
{!mediaBlobUrl && ( { ! mediaBlobUrl && (
<div className='webwrap-start-border'> <div className='webwrap-start-border'>
<button <button
onClick={ onClick={
status === 'recording' status === 'recording'
? stopRecording ? stopRecording
: startRecording : startRecording
} }
className={ className={
status === 'recording' status === 'recording'
? 'webwrap-record-center' ? 'webwrap-record-center'
: 'webwrap-start-center' : 'webwrap-start-center'
} }
/> />
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className='web-bottom-elements'> <div className='web-bottom-elements'>
{mediaBlobUrl && ( {mediaBlobUrl && (
<div onClick={clearBlobUrl} className='web-button-wrap'> <div onClick={clearBlobUrl} className='web-button-wrap'>
<div className='web-button-rerecord' /> <div className='web-button-rerecord'/>
<span className='web-button-rerecord-text'> <span className='web-button-rerecord-text'>
Перезаписать Перезаписать
</span> </span>
</div> </div>
)} )}
{!mediaBlobUrl && <div className='web-button-spacer' />} { ! mediaBlobUrl && <div className='web-button-spacer'/>}
{!mediaBlobUrl && ( { ! mediaBlobUrl && (
<div <div
onClick={isAudioMuted ? unMuteAudio : muteAudio} onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={ className={
isAudioMuted ? 'web-button-unmute' : 'web-button-mute' isAudioMuted ? 'web-button-unmute' : 'web-button-mute'
} }
/> />
)} )}
<div className='web-button-spacer' /> <div className='web-button-spacer'/>
</div> </div>
</> </>
)} )}
</Fragment> </Fragment>
</> </>
) )
default:
return <div>Пусто</div>
}
}
const isDisabledAction = () => {
let isDisabled = false
switch (innerModalType) {
case 'video':
case 'image':
if (uploadOptions.url === null || uploadedPaths.length === 0) {
isDisabled = true
}
break
case 'screencust':
if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true
}
break
case 'voicemessage':
if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true
}
break
case 'webcamera':
if (status === 'recording' || isUploading || !mediaBlobUrl) {
isDisabled = true
}
break
case 'iframe':
try {
const url = new URL(embedContent)
switch (url.hostname) {
case 'rutube.ru':
case 'www.rutube.ru':
case 'vimeo.com':
case 'ok.ru':
case 'www.ok.ru':
case 'youtu.be':
case 'youtube.com':
case 'www.youtube.com':
break
default: default:
isDisabled = true return <div>Пусто</div>
}
} catch (error) {
isDisabled = true
} }
break
case 'iframe_custom':
const regex = new RegExp(
'(?:<iframe[^>]*)(?:(?:\\/>)|(?:>.*?<\\/iframe>))'
)
isDisabled = !regex.test(embedContent)
break
} }
return isDisabled const isDisabledAction = () => {
} let isDisabled = false
if (!editor) {
return null
}
const buttons = switch (innerModalType) {
innerModalType === 'remove_iframe' case 'video':
? [ case 'image':
{ if (uploadOptions.url === null || uploadedPaths.length === 0) {
title: 'Отмена', isDisabled = true
className: ' atma-editor-cancel', }
onClick: () => { break
stopRecording() case 'screencust':
unMuteAudio() if (status === 'recording' || isUploading || ! mediaBlobUrl) {
clearBlobUrl() isDisabled = true
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
},
{
title: 'Удалить',
className: ' atma-editor-complete',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
}
]
: [
{
title: 'Отмена',
className: ' atma-editor-cancel',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
},
{
title:
mediaBlobUrl && uploadedPaths.length === 0
? isUploading
? 'Сохранение...'
: 'Вставить'
: 'Вставить',
className: ' atma-editor-complete',
onClick: async () => {
if (status === 'recording' || isUploading) {
return false
} else {
if (
document.querySelectorAll('.atma-editor-uploader-progress')
.length > 0
) {
if (
// eslint-disable-next-line no-undef
!confirm(
'Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?'
)
) {
return false
}
} }
break
case 'voicemessage':
if (status === 'recording' || isUploading || ! mediaBlobUrl) {
isDisabled = true
}
break
case 'webcamera':
if (status === 'recording' || isUploading || ! mediaBlobUrl) {
isDisabled = true
}
break
case 'iframe':
try { try {
switch (innerModalType) { const url = new URL(embedContent)
case 'image':
uploadedPaths.map((file, i) => {
editor.chain().focus().setImage({ src: file.path }).run();
})
break
case 'video':
uploadedPaths.map((file, i) => {
editor
.chain()
.focus()
.setVideo({
src: file.path,
poster: file.path + '.jpg'
})
.run()
})
break
case 'voicemessage':
if (mediaBlobUrl && uploadedPaths.length === 0) {
if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) {
editor
.chain()
.focus()
.addVoiceMessage({ src: data.file_path })
.run()
}
})
}
}
break
case 'screencust':
if (mediaBlobUrl && uploadedPaths.length === 0) {
if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) {
editor
.chain()
.focus()
.setVideo({ src: data.file_path })
.run()
}
})
}
}
break
case 'webcamera':
if (mediaBlobUrl && uploadedPaths.length === 0) {
if (!isUploading) {
await saveScreenCust(mediaBlobUrl).then((data) => {
if (data?.file_path) {
editor
.chain()
.focus()
.setVideo({ src: data.file_path })
.run()
}
})
}
}
break
case 'iframe':
let _url = embedContent
const reg = /(http|https):\/\/([\w.]+\/?)\S*/
const url = new URL(
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}`
break
case 'vimeo.com': case 'vimeo.com':
_url = `https://player.vimeo.com/video/${urlId}`
break
case 'ok.ru': case 'ok.ru':
case 'www.ok.ru': case 'www.ok.ru':
_url = `//ok.ru/videoembed/${urlId}`
break
case 'youtu.be': case 'youtu.be':
case 'youtube.com': case 'youtube.com':
case 'www.youtube.com': case 'www.youtube.com':
if ( break
url.hostname.indexOf('youtu.be') === -1 && default:
url.search !== '' isDisabled = true
) { }
if (url.searchParams.get('v')) { } catch (error) {
urlId = url.searchParams.get('v') isDisabled = true
}
break
case 'iframe_custom':
const regex = new RegExp(
'(?:<iframe[^>]*)(?:(?:\\/>)|(?:>.*?<\\/iframe>))'
)
isDisabled = ! regex.test(embedContent)
break
}
return isDisabled
}
if ( ! editor) {
return null
}
const buttons =
innerModalType === 'remove_iframe'
? [
{
title: 'Отмена',
className: ' atma-editor-cancel',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
},
{
title: 'Удалить',
className: ' atma-editor-complete',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
}
]
: [
{
title: 'Отмена',
className: ' atma-editor-cancel',
onClick: () => {
stopRecording()
unMuteAudio()
clearBlobUrl()
setUploaderUid(`uid${new Date()}`)
setUploadedPaths([])
setModalIsOpen(false)
}
},
{
title:
mediaBlobUrl && uploadedPaths.length === 0
? isUploading
? 'Сохранение...'
: 'Вставить'
: 'Вставить',
className: ' atma-editor-complete',
onClick: async () => {
if (status === 'recording' || isUploading) {
return false
} else {
if (
document.querySelectorAll('.atma-editor-uploader-progress').length > 0
) {
if (
// eslint-disable-next-line no-undef
! confirm(
'Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?'
)
) {
return false
}
} }
} try {
_url = `https://www.youtube.com/embed/${urlId}` switch (innerModalType) {
break case 'image':
} uploadedPaths.map((file, i) => {
editor.chain().focus().setIframe({ src: _url }).run() editor.chain().focus().setImage({src: file.path}).run();
break })
case 'iframe_custom': break
editor.chain().focus().insertContent(embedContent).run() case 'video':
break uploadedPaths.map((file, i) => {
case 'iframe_pptx': editor.chain().focus().setVideo({
uploadedPaths.map((file, i) => { src: file.path,
editor poster: file.path + '.jpg'
.chain() }).run()
.focus() })
.insertContent( break
`<iframe src="https://view.officeapps.live.com/op/embed.aspx?src=${file.path}" width="100%" height="600px" frameBorder="0"></iframe>` case 'voicemessage':
) if (mediaBlobUrl && uploadedPaths.length === 0) {
.run() if ( ! isUploading) {
}) await saveScreenCust(mediaBlobUrl).then((data) => {
break if (data?.file_path) {
case 'audio': editor.chain().focus().addVoiceMessage({src: data.file_path}).run()
uploadedPaths.map((file) => { }
editor })
.chain() }
.focus() }
.insertContent( break
`<audio class="audio-player" controls="true" src="${file.path}" />` case 'screencust':
) if (mediaBlobUrl && uploadedPaths.length === 0) {
.run() if ( ! isUploading) {
}) await saveScreenCust(mediaBlobUrl).then((data) => {
break if (data?.file_path) {
case 'iframe_pdf': editor.chain().focus().setVideo({src: data.file_path}).run()
uploadedPaths.map((file, i) => { }
editor })
.chain() }
.focus() }
.insertContent( break
`<iframe src="https://docs.google.com/viewer?embedded=true&url=${file.path}" width="100%" height="800px" frameBorder="0"></iframe>` case 'webcamera':
) if (mediaBlobUrl && uploadedPaths.length === 0) {
.run() if ( ! isUploading) {
}) await saveScreenCust(mediaBlobUrl).then((data) => {
break if (data?.file_path) {
case 'file': editor.chain().focus().setVideo({src: data.file_path}).run()
uploadedPaths.map((file, i) => { }
let exp = file.path.split('.') })
exp = exp[exp.length - 1] }
editor }
.chain() break
.focus() case 'iframe':
.insertContent( let _url = embedContent
` const reg = /(http|https):\/\/([\w.]+\/?)\S*/
const url = new URL(
reg.test(_url) ? _url : 'https:' + _url
)
let urlId = url.pathname.replace(/\/$/gi, '').split('/').pop()
switch (url.hostname) {
case 'rutube.ru':
case 'www.rutube.ru':
_url = `https://rutube.ru/pl/?pl_id&pl_type&pl_video=${urlId}`
break
case 'vimeo.com':
_url = `https://player.vimeo.com/video/${urlId}`
break
case 'ok.ru':
case 'www.ok.ru':
_url = `//ok.ru/videoembed/${urlId}`
break
case 'youtu.be':
case 'youtube.com':
case 'www.youtube.com':
if (
url.hostname.indexOf('youtu.be') === -1 &&
url.search !== ''
) {
if (url.searchParams.get('v')) {
urlId = url.searchParams.get('v')
}
}
_url = `https://www.youtube.com/embed/${urlId}`
break
}
editor.chain().focus().setIframe({src: _url}).run()
break
case 'iframe_custom':
editor.chain().focus().insertContent(embedContent).run()
break
case 'iframe_pptx':
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()
})
break
case 'audio':
uploadedPaths.map((file) => {
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
case 'file':
uploadedPaths.map((file, i) => {
let exp = file.path.split('.')
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> <a href="${file.path}" target="_blank" download="${file.name}.${exp}" data-size="${file.size}">${file.name}</a>
` `
) ).run()
.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) { return (
items = ['alignLeft', 'alignCenter', 'alignRight'] <div className='atma-editor-wrap' style={style}>
} <div className='atma-editor'>
setFocusFromTo([o.from, o.to].join(':')) <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 (items.length > 0) { if (editor.isActive('image') === true) {
setBubbleItems(items) items = ['alignLeft', 'alignCenter', 'alignRight']
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) setFocusFromTo([o.from, o.to].join(':'))
}}
/> if (items.length > 0) {
) setBubbleItems(items)
}) return true
: 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} tippyOptions={{duration: 100}}
/> >
) <div
} className='atma-editor-bubble'
})} onClick={(e) => e.stopPropagation()}
</div> >
</BubbleMenu> {colorsSelected !== null
<EditorContent editor={editor} className='atma-editor-content' /> ? colors[colorsSelected].map((itemColor, i) => {
</div> return (
<EditorModal isOpen={modalIsOpen} title={modalTitle}> <div
{getInnerModal()} key={'colors' + colorsSelected + i}
{buildActionsModal(buttons)} className={
</EditorModal> 'qcolors' + (itemColor === 'none' ? ' unset' : '')
</div> }
) 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>
</div>
)
} }
export default QEditor export default QEditor
...@@ -3,157 +3,137 @@ import { Extension } from "@tiptap/core"; ...@@ -3,157 +3,137 @@ import { Extension } from "@tiptap/core";
// loads with editor // loads with editor
export default Extension.create({ export default Extension.create({
name: "Resizable", name: "Resizable",
addOptions() { addOptions () {
return { return {
types: ["image", "video"], types: ["image", "video"],
handlerStyle: { handlerStyle: {
width: "8px", width: "8px",
height: "8px", height: "8px",
background: "#07c160", background: "#07c160",
}, },
layerStyle: { layerStyle: {
border: "2px solid #07c160", border: "2px solid #07c160",
}, },
}; };
}, },
addStorage() {
return {
resizeElement: null,
};
},
onCreate({ editor }) { addStorage () {
const element = editor.options.element; return {
element.style.position = "relative"; resizeElement: null,
// resizeLayer
const resizeLayer = document.createElement("div");
resizeLayer.className = "resize-layer";
resizeLayer.style.display = "none";
resizeLayer.style.position = "absolute";
Object.entries(this.options.layerStyle).forEach(([key, value]) => {
resizeLayer.style[key] = value;
});
resizeLayer.addEventListener("mousedown", (e) => {
const resizeElement = this.storage.resizeElement;
if (!resizeElement) return;
if (/bottom/.test(e.target.className)) {
let startX = e.screenX;
const dir = e.target.classList.contains("bottom-left") ? -1 : 1;
const mousemoveHandle = (e) => {
e.preventDefault();
const width = resizeElement.clientWidth;
const distanceX = e.screenX - startX;
const total = width + dir * distanceX;
// resizeElement
resizeElement.style.width = total + "px";
const clientWidth = resizeElement.clientWidth;
const clientHeight = resizeElement.clientHeight;
resizeElement.style.width = clientWidth + "px"; // max width
// resizeLayer
const pos = getRelativePosition(resizeElement, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = clientWidth + "px";
resizeLayer.style.height = clientHeight + "px";
startX = e.screenX;
}; };
document.addEventListener("mousemove", mousemoveHandle); },
document.addEventListener("mouseup", () =>{
document.removeEventListener("mousemove", mousemoveHandle) onCreate ({editor}) {
}); const element = editor.options.element;
} element.style.position = "relative";
}); // resizeLayer
const handlers = ["top-left", "top-right", "bottom-left", "bottom-right"]; console.log('onCreate', editor);
const fragment = document.createDocumentFragment();
for (let name of handlers) { const resizeLayer = document.createElement("div");
const item = document.createElement("div"); resizeLayer.className = "resize-layer";
item.className = `handler ${name}`; resizeLayer.style.display = "none";
item.style.position = "absolute"; resizeLayer.style.position = "absolute";
Object.entries(this.options.handlerStyle).forEach(([key, value]) => {
item.style[key] = value; Object.entries(this.options.layerStyle).forEach(([key, value]) => {
}); resizeLayer.style[key] = value;
const dir = name.split("-"); });
item.style[dir[0]] = parseInt(item.style.width) / -2 + "px"; resizeLayer.addEventListener("mousedown", (e) => {
item.style[dir[1]] = parseInt(item.style.height) / -2 + "px"; const resizeElement = this.storage.resizeElement;
if (name === "bottom-left") item.style.cursor = "sw-resize";
if (name === "bottom-right") item.style.cursor = "se-resize"; if ( ! resizeElement) return;
fragment.appendChild(item); if (/bottom/.test(e.target.className)) {
} let startX = e.screenX;
resizeLayer.appendChild(fragment); const dir = e.target.classList.contains("bottom-left") ? -1 : 1;
editor.resizeLayer = resizeLayer; const mousemoveHandle = (e) => {
element.appendChild(resizeLayer); e.preventDefault();
const width = resizeElement.clientWidth;
const distanceX = e.screenX - startX;
const total = width + dir * distanceX;
}, // resizeElement
resizeElement.style.width = total + "px";
onSelectionUpdate({ editor, transaction }) { const clientWidth = resizeElement.clientWidth;
const clientHeight = resizeElement.clientHeight;
resizeElement.style.width = clientWidth + "px"; // max width
// resizeLayer
const pos = getRelativePosition(resizeElement, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = clientWidth + "px";
resizeLayer.style.height = clientHeight + "px";
startX = e.screenX;
};
document.addEventListener("mousemove", mousemoveHandle);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", mousemoveHandle)
});
}
});
const handlers = ["top-left", "top-right", "bottom-left", "bottom-right"];
const fragment = document.createDocumentFragment();
for (let name of handlers) {
const item = document.createElement("div");
item.className = `handler ${name}`;
item.style.position = "absolute";
Object.entries(this.options.handlerStyle).forEach(([key, value]) => {
item.style[key] = value;
});
const dir = name.split("-");
item.style[dir[0]] = parseInt(item.style.width) / -2 + "px";
item.style[dir[1]] = parseInt(item.style.height) / -2 + "px";
if (name === "bottom-left") item.style.cursor = "sw-resize";
if (name === "bottom-right") item.style.cursor = "se-resize";
fragment.appendChild(item);
}
resizeLayer.appendChild(fragment);
editor.resizeLayer = resizeLayer;
element.appendChild(resizeLayer);
},
onUpdate({editor, transaction}) {
selectionUpdate({editor, transaction}, this.options, this.storage)
},
onSelectionUpdate ({editor, transaction}) {
selectionUpdate({editor, transaction}, this.options, this.storage)
},
});
function selectionUpdate({editor, transaction}, options, storage)
{
const element = editor.options.element; const element = editor.options.element;
const node = transaction.curSelection.node; const node = transaction.curSelection.node;
const resizeLayer = editor.resizeLayer; const resizeLayer = editor.resizeLayer;
console.log("res layer",resizeLayer) // console.log("res layer", resizeLayer)
if (node && this.options.types.includes(node.type.name)) { if (node && options.types.includes(node.type.name)) {
// resizeLayer位置大小 // resizeLayer
resizeLayer.style.display = "block"; resizeLayer.style.display = "block";
let dom = editor.view.domAtPos(transaction.curSelection.from).node; let dom = editor.view.domAtPos(transaction.curSelection.from).node;
if (dom.getAttribute("src") !== node.attrs.src) { if (dom.getAttribute("src") !== node.attrs.src) {
dom = dom.querySelector(`[src="${node.attrs.src}"]`); dom = dom.querySelector(`[src="${node.attrs.src}"]`);
} }
this.storage.resizeElement = dom; storage.resizeElement = dom;
const pos = getRelativePosition(dom, element);
const pos = getRelativePosition(dom, element);
console.log("dom after src", dom)
console.log(pos)
resizeLayer.style.top = pos.top + "px"; resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px"; resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = dom.width + "px"; resizeLayer.style.width = dom.width + "px";
resizeLayer.style.height = dom.height + "px"; resizeLayer.style.height = dom.height + "px";
} else {
const resizeObserver = new ResizeObserver((entries) => { resizeLayer.style.display = "none";
for (let entry of entries) {
const currentPosition = entry.target.getBoundingClientRect();
if (currentPosition.x === 0) {
console.log("changed pos");
//resizeLayer.style.display = "none";
}
console.log(entry.target.parentNode)
console.log("target: ", entry.target)
console.log("bounding rect: ", currentPosition)
}
})
resizeObserver.observe(dom);
} else {
console.log("no node");
resizeLayer.style.display = "none";
} }
}, }
}); function getRelativePosition (element, ancestor) {
const elementRect = element.getBoundingClientRect();
const ancestorRect = ancestor.getBoundingClientRect();
const relativePosition = {
// 计算相对位置 top: parseInt(elementRect.top - ancestorRect.top + ancestor.scrollTop),
function getRelativePosition(element, ancestor) { left: parseInt(elementRect.left - ancestorRect.left + ancestor.scrollLeft),
const elementRect = element.getBoundingClientRect(); };
const ancestorRect = ancestor.getBoundingClientRect(); return relativePosition;
const relativePosition = { }
top: parseInt(elementRect.top - ancestorRect.top + ancestor.scrollTop),
left: parseInt(elementRect.left - ancestorRect.left + ancestor.scrollLeft),
};
return relativePosition;
}
\ No newline at end of file
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