Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
lib
react-ag-qeditor
Commits
cbdf4e4b
Commit
cbdf4e4b
authored
Oct 09, 2023
by
DenSakh
Browse files
Merge branch '2694_iframe_remove_button' into tiptap
# Conflicts: # src/QEditor.jsx
parents
ce97b7dc
a5f0c2db
Changes
5
Show whitespace changes
Inline
Side-by-side
src/QEditor.jsx
View file @
cbdf4e4b
/* eslint-disable no-undef */
/* eslint-disable no-case-declarations */
import
React
,
{
Fragment
,
useEffect
,
useState
,
useRef
}
from
'
react
'
import
'
./index.scss
'
// import EditorModal from "./components/EditorModal"
...
...
@@ -13,54 +15,72 @@ import TableHeader from '@tiptap/extension-table-header'
import
Focus
from
'
@tiptap/extension-focus
'
// import Link from '@tiptap/extension-link'
import
Image
from
'
@tiptap/extension-image
'
import
TextAlign
from
'
@tiptap/extension-text-align
'
;
import
{
Color
}
from
'
@tiptap/extension-color
'
;
import
Highlight
from
'
@tiptap/extension-highlight
'
;
import
TextStyle
from
'
@tiptap/extension-text-style
'
;
import
Superscript
from
"
@tiptap/extension-superscript
"
;
import
Subscript
from
"
@tiptap/extension-subscript
"
;
import
TextAlign
from
'
@tiptap/extension-text-align
'
import
{
Color
}
from
'
@tiptap/extension-color
'
import
Highlight
from
'
@tiptap/extension-highlight
'
import
TextStyle
from
'
@tiptap/extension-text-style
'
import
Superscript
from
'
@tiptap/extension-superscript
'
import
Subscript
from
'
@tiptap/extension-subscript
'
import
ToolBar
from
"
./components/ToolBar
"
import
EditorModal
from
"
./components/EditorModal
"
import
Uploader
from
"
./components/Uploader
"
import
ToolBar
from
'
./components/ToolBar
'
import
EditorModal
from
'
./components/EditorModal
'
import
Uploader
from
'
./components/Uploader
'
import
Video
from
'
./extensions/Video
'
import
Iframe
from
'
./extensions/Iframe
'
import
CustomLink
from
'
./extensions/CustomLink
'
import
DragAndDrop
from
"
./extensions/DragAndDrop
"
;
import
{
useReactMediaRecorder
}
from
"
react-media-recorder
"
;
import
axios
from
"
axios
"
;
import
ReactStopwatch
from
'
react-stopwatch
'
;
import
Audio
from
"
./extensions/Audio
"
;
import
DragAndDrop
from
'
./extensions/DragAndDrop
'
import
{
useReactMediaRecorder
}
from
'
react-media-recorder
'
import
axios
from
'
axios
'
import
ReactStopwatch
from
'
react-stopwatch
'
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
=
({
value
,
onChange
=
()
=>
{},
style
,
uploadOptions
=
{
url
:
""
,
errorMessage
:
""
},
toolsOptions
=
{
type
:
'
all
'
}
uploadOptions
=
{
url
:
''
,
errorMessage
:
''
},
toolsOptions
=
{
type
:
'
all
'
}
})
=>
{
global
.
uploadUrl
=
uploadOptions
.
url
;
global
.
uploadUrl
=
uploadOptions
.
url
const
[
innerModalType
,
setInnerModalType
]
=
useState
(
null
)
;
const
[
embedContent
,
setEmbedContent
]
=
useState
(
''
)
;
const
[
uploaderUid
,
setUploaderUid
]
=
useState
(
'
uid
'
+
new
Date
())
;
const
[
uploadedPaths
,
setUploadedPaths
]
=
useState
([])
;
const
[
modalIsOpen
,
setModalIsOpen
]
=
useState
(
false
)
;
const
[
modalTitle
,
setModalTitle
]
=
useState
(
''
)
;
const
[
bubbleItems
,
setBubbleItems
]
=
useState
(
initialBubbleItems
)
;
const
[
colorsSelected
,
setColorsSelected
]
=
useState
(
null
)
;
const
[
focusFromTo
,
setFocusFromTo
]
=
useState
(
null
)
;
const
[
oldFocusFromTo
,
setOldFocusFromTo
]
=
useState
(
null
)
;
const
[
isUploading
,
setIsUploading
]
=
useState
(
false
)
;
const
[
recordType
,
setRecordType
]
=
useState
({
video
:
true
})
const
[
innerModalType
,
setInnerModalType
]
=
useState
(
null
)
const
[
embedContent
,
setEmbedContent
]
=
useState
(
''
)
const
[
uploaderUid
,
setUploaderUid
]
=
useState
(
'
uid
'
+
new
Date
())
const
[
uploadedPaths
,
setUploadedPaths
]
=
useState
([])
const
[
modalIsOpen
,
setModalIsOpen
]
=
useState
(
false
)
const
[
modalTitle
,
setModalTitle
]
=
useState
(
''
)
const
[
bubbleItems
,
setBubbleItems
]
=
useState
(
initialBubbleItems
)
const
[
colorsSelected
,
setColorsSelected
]
=
useState
(
null
)
const
[
focusFromTo
,
setFocusFromTo
]
=
useState
(
null
)
const
[
oldFocusFromTo
,
setOldFocusFromTo
]
=
useState
(
null
)
const
[
isUploading
,
setIsUploading
]
=
useState
(
false
)
const
[
recordType
,
setRecordType
]
=
useState
({
video
:
true
})
// eslint-disable-next-line no-unused-vars
const
getRgb
=
(
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
;
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
}
const
{
status
,
...
...
@@ -72,27 +92,27 @@ const QEditor = ({
unMuteAudio
,
isAudioMuted
,
clearBlobUrl
}
=
useReactMediaRecorder
(
recordType
)
;
}
=
useReactMediaRecorder
(
recordType
)
const
videoRef
=
useRef
(
null
)
;
const
videoRef
=
useRef
(
null
)
useEffect
(()
=>
{
if
(
videoRef
.
current
&&
previewStream
)
{
videoRef
.
current
.
srcObject
=
previewStream
;
videoRef
.
current
.
srcObject
=
previewStream
}
},
[
previewStream
])
;
},
[
previewStream
])
useEffect
(()
=>
{
if
(
focusFromTo
!==
oldFocusFromTo
)
{
setColorsSelected
(
null
)
setOldFocusFromTo
(
focusFromTo
)
;
setOldFocusFromTo
(
focusFromTo
)
}
},
[
focusFromTo
])
const
modalOpener
=
(
type
,
title
)
=>
{
setModalTitle
(
title
)
;
setInnerModalType
(
type
)
;
setModalIsOpen
(
true
)
;
setModalTitle
(
title
)
setInnerModalType
(
type
)
setModalIsOpen
(
true
)
}
const
colors
=
{
color
:
[
...
...
@@ -125,14 +145,14 @@ const QEditor = ({
'
#dea75b
'
,
'
#ffe672
'
]
}
;
}
const
toolsLib
=
{
link
:
{
title
:
'
Вставить ссылку
'
,
onClick
:
()
=>
{
const
previousUrl
=
editor
.
getAttributes
(
'
link
'
).
href
const
url
=
window
.
prompt
(
'
Введите URL
'
,
previousUrl
)
;
const
url
=
window
.
prompt
(
'
Введите URL
'
,
previousUrl
)
// cancelled
if
(
url
===
null
)
{
...
...
@@ -141,13 +161,17 @@ const QEditor = ({
// empty
if
(
url
===
''
)
{
editor
.
chain
().
focus
().
extendMarkRange
(
'
link
'
).
unsetLink
().
run
();
editor
.
chain
().
focus
().
extendMarkRange
(
'
link
'
).
unsetLink
().
run
()
return
}
// 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
:
{
...
...
@@ -170,29 +194,29 @@ const QEditor = ({
title
:
'
Вставить презентацию pptx
'
,
onClick
:
()
=>
modalOpener
(
'
iframe_pptx
'
,
'
Вставить презентацию pptx
'
)
},
audio
:
{
title
:
'
Вставить аудио файл
'
,
onClick
:
()
=>
modalOpener
(
'
audio
'
,
'
Вставить аудио файл
'
)
},
iframe_pdf
:
{
title
:
'
Вставить презентацию pdf
'
,
onClick
:
()
=>
modalOpener
(
'
iframe_pdf
'
,
'
Вставить презентацию pdf
'
)
},
audio
:
{
title
:
'
Вставить аудио файл
'
,
onClick
:
()
=>
modalOpener
(
'
audio
'
,
'
Вставить аудио файл
'
)
},
image
:
{
title
:
'
Загрузить изображение
'
,
onClick
:
()
=>
modalOpener
(
'
image
'
,
'
Загрузить изображение
'
)
},
h2
:
{
title
:
'
Заголовок 2
'
,
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
2
}).
run
()
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
2
}).
run
()
},
h3
:
{
title
:
'
Заголовок 3
'
,
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
3
}).
run
()
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
3
}).
run
()
},
h4
:
{
title
:
'
Заголовок 4
'
,
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
4
}).
run
()
onClick
:
()
=>
editor
.
chain
().
focus
().
toggleHeading
({
level
:
4
}).
run
()
},
paragraph
:
{
title
:
'
Обычный
'
,
...
...
@@ -261,27 +285,28 @@ const QEditor = ({
alignLeft
:
{
title
:
'
По левому краю
'
,
onClick
:
()
=>
{
editor
.
commands
.
setTextAlign
(
'
left
'
)
;
editor
.
chain
().
focus
()
;
editor
.
commands
.
setTextAlign
(
'
left
'
)
editor
.
chain
().
focus
()
}
},
alignCenter
:
{
title
:
'
По центру
'
,
onClick
:
()
=>
{
editor
.
commands
.
setTextAlign
(
'
center
'
)
editor
.
chain
().
focus
()
;
editor
.
chain
().
focus
()
}
},
alignRight
:
{
title
:
'
По правому краю
'
,
onClick
:
()
=>
{
editor
.
commands
.
setTextAlign
(
'
right
'
)
;
editor
.
chain
().
focus
()
;
editor
.
commands
.
setTextAlign
(
'
right
'
)
editor
.
chain
().
focus
()
}
},
insertTable
:
{
title
:
'
Вставить таблицу
'
,
onClick
:
()
=>
editor
.
chain
().
focus
().
insertTable
({
rows
:
2
,
cols
:
2
}).
run
()
onClick
:
()
=>
editor
.
chain
().
focus
().
insertTable
({
rows
:
2
,
cols
:
2
}).
run
()
},
deleteTable
:
{
title
:
'
Удалить таблицу
'
,
...
...
@@ -323,7 +348,7 @@ const QEditor = ({
title
:
'
Цвет текста
'
,
onClick
:
()
=>
{
setColorsSelected
(
'
color
'
)
editor
.
chain
().
focus
()
;
editor
.
chain
().
focus
()
}
},
highlight
:
{
...
...
@@ -333,7 +358,7 @@ const QEditor = ({
voicemessage
:
{
title
:
'
Записать голосовое сообщение
'
,
onClick
:
()
=>
{
setRecordType
({
audio
:
true
})
setRecordType
({
audio
:
true
})
clearBlobUrl
()
modalOpener
(
'
voicemessage
'
,
'
Записать голосовое сообщение
'
)
}
...
...
@@ -341,7 +366,7 @@ const QEditor = ({
webcamera
:
{
title
:
'
Записать с камеры
'
,
onClick
:
()
=>
{
setRecordType
({
video
:
true
})
setRecordType
({
video
:
true
})
clearBlobUrl
()
modalOpener
(
'
webcamera
'
,
'
Записать с камеры
'
)
}
...
...
@@ -350,14 +375,14 @@ const QEditor = ({
title
:
'
Записать экран
'
,
onClick
:
()
=>
{
if
(
isMobile
)
{
setRecordType
({
video
:
true
})
setRecordType
({
video
:
true
})
}
else
{
setRecordType
({
screen
:
true
})
setRecordType
({
screen
:
true
})
}
clearBlobUrl
()
modalOpener
(
'
screencust
'
,
'
Записать экран
'
)
}
}
,
}
// katex: {
// title: 'Вставить формулу',
// onClick: () => {
...
...
@@ -394,11 +419,11 @@ const QEditor = ({
TextAlign
.
configure
({
defaultAlignment
:
'
left
'
,
types
:
[
'
heading
'
,
'
paragraph
'
],
alignments
:
[
'
left
'
,
'
center
'
,
'
right
'
,
'
justify
'
]
,
alignments
:
[
'
left
'
,
'
center
'
,
'
right
'
,
'
justify
'
]
}),
TextStyle
,
Color
.
configure
({
types
:
[
'
textStyle
'
]
,
types
:
[
'
textStyle
'
]
}),
Highlight
.
configure
({
multicolor
:
true
...
...
@@ -409,7 +434,7 @@ const QEditor = ({
}),
Focus
.
configure
({
className
:
'
atma-editor-focused
'
,
mode
:
"
all
"
mode
:
'
all
'
}),
DragAndDrop
.
configure
({
linkUpload
:
uploadOptions
.
url
...
...
@@ -419,265 +444,295 @@ const QEditor = ({
Subscript
],
content
:
value
,
onUpdate
:
({
editor
})
=>
onChange
(
editor
.
getHTML
()),
onFocus
:
({
editor
})
=>
{
le
t
wrap
=
editor
.
options
.
element
.
closest
(
'
.atma-editor-wrap
'
)
;
onUpdate
:
({
editor
})
=>
onChange
(
editor
.
getHTML
()),
onFocus
:
({
editor
})
=>
{
cons
t
wrap
=
editor
.
options
.
element
.
closest
(
'
.atma-editor-wrap
'
)
wrap
.
querySelectorAll
(
'
.atma-editor-toolbar-s
'
).
forEach
(
function
(
s
)
{
s
.
classList
.
remove
(
'
show
'
);
});
s
.
classList
.
remove
(
'
show
'
)
})
}
})
const
buildActionsModal
=
(
buttons
=
[])
=>
{
if
(
buttons
.
length
===
0
)
{
return
null
;
return
null
}
return
(
<
div
className
=
{
'
atma-editor-modal-action
'
}
>
{
buttons
.
map
((
btn
,
i
)
=>
(
<
button
disabled
=
{
btn
.
disabled
}
type
=
{
'
button
'
}
key
=
{
'
mAction
'
+
i
}
<
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
>
))
}
onClick
=
{
btn
.
onClick
}
>
{
btn
.
title
}
</
button
>
))
}
</
div
>
)
}
const
getUploader
=
({
accept
=
'
*
'
,
...
o
})
=>
{
let
url
=
uploadOptions
.
url
,
multiple
=
true
;
const
getUploader
=
({
accept
=
'
*
'
,
...
o
})
=>
{
let
url
=
uploadOptions
.
url
let
multiple
=
true
if
(
o
.
afterParams
&&
o
.
afterParams
.
length
>
0
)
{
if
(
uploadOptions
.
url
.
indexOf
(
'
?
'
)
!==
-
1
)
{
url
=
uploadOptions
.
url
+
'
&
'
+
o
.
afterParams
.
join
(
'
&
'
)
;
url
=
uploadOptions
.
url
+
'
&
'
+
o
.
afterParams
.
join
(
'
&
'
)
}
else
{
url
=
uploadOptions
.
url
+
'
?
'
+
o
.
afterParams
.
join
(
'
&
'
)
;
url
=
uploadOptions
.
url
+
'
?
'
+
o
.
afterParams
.
join
(
'
&
'
)
}
}
if
(
typeof
o
.
multiple
!==
'
undefined
'
)
{
multiple
=
o
.
multiple
;
multiple
=
o
.
multiple
}
return
<
Uploader
return
(
<
Uploader
key
=
{
uploaderUid
}
accept
=
{
accept
}
action
=
{
url
}
errorMessage
=
{
uploadOptions
.
errorMessage
}
onSuccess
=
{
(
file
)
=>
{
let
_uploadedPaths
=
[...
uploadedPaths
];
_uploadedPaths
.
push
(
file
);
const
_uploadedPaths
=
[...
uploadedPaths
]
_uploadedPaths
.
push
(
file
)
setUploadedPaths
(
_uploadedPaths
)
}
}
onDelete
=
{
(
deleteFile
)
=>
{
let
deleteIdx
=
null
;
le
t
_uploadedPaths
=
[...
uploadedPaths
]
;
let
deleteIdx
=
null
cons
t
_uploadedPaths
=
[...
uploadedPaths
]
_uploadedPaths
.
map
((
f
,
i
)
=>
{
if
(
f
.
uid
===
deleteFile
.
uid
)
{
deleteIdx
=
i
;
deleteIdx
=
i
}
})
;
_uploadedPaths
.
splice
(
deleteIdx
,
1
)
;
})
_uploadedPaths
.
splice
(
deleteIdx
,
1
)
setUploadedPaths
(
_uploadedPaths
)
}
}
multiple
=
{
multiple
}
modalType
=
{
innerModalType
}
/>
)
}
const
saveScreenCust
=
async
(
fileBlob
)
=>
{
if
(
fileBlob
)
{
setIsUploading
(
true
)
le
t
blobData
=
await
fetch
(
fileBlob
).
then
((
res
)
=>
res
.
blob
())
;
cons
t
blobData
=
await
fetch
(
fileBlob
).
then
((
res
)
=>
res
.
blob
())
const
data
=
new
FormData
();
let
file
=
new
File
([
blobData
],
"
name.
"
+
(
recordType
?.
audio
?
"
mp3
"
:
"
webm
"
));
data
.
append
(
"
file
"
,
file
);
const
data
=
new
FormData
()
const
file
=
new
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
)
{
axios
.
post
(
uploadOptions
.
url
,
data
,
{
headers
:
headers
}).
then
(
response
=>
{
if
(
response
.
data
.
state
===
"
success
"
)
{
axios
.
post
(
uploadOptions
.
url
,
data
,
{
headers
:
headers
})
.
then
((
response
)
=>
{
if
(
response
.
data
.
state
===
'
success
'
)
{
resolve
(
response
.
data
)
}
setIsUploading
(
false
)
});
})
})
}
}
};
const
getInnerModal
=
()
=>
{
switch
(
innerModalType
)
{
case
'
iframe
'
:
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
>
<
IframeModal
embedContent
=
{
embedContent
}
setEmbedContent
=
{
setEmbedContent
}
/>
)
case
'
iframe_custom
'
:
return
(
<
F
ra
g
me
nt
>
<
textarea
style
=
{
{
width
:
'
100%
'
,
height
:
'
100%
'
}
}
rows
=
{
18
}
value
=
{
embedContent
}
placeholder
=
{
'
<iframe></iframe>
'
}
onInput
=
{
(
e
)
=>
setEmbedContent
(
e
.
target
.
value
)
}
<
If
rame
CustomModal
embedContent
=
{
embedContent
}
setEmbedContent
=
{
setEmbedContent
}
/>
</
Fragment
>
)
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
>
<
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
>
<
Fragment
>
{
getUploader
({
accept
:
'
.wav, .mp3, .ogg
'
})
}
</
Fragment
>
)
case
'
iframe_pdf
'
:
return
(
<
Fragment
>
{
getUploader
({
accept
:
'
application/pdf
'
,
afterParams
:
[
'
no_convert=1
'
]})
}
</
Fragment
>
<
Fragment
>
{
getUploader
({
accept
:
'
application/pdf
'
,
afterParams
:
[
'
no_convert=1
'
]
})
}
</
Fragment
>
)
case
'
video
'
:
return
(
<
Fragment
>
{
getUploader
({
accept
:
'
video/*
'
})
}
</
Fragment
>
)
return
<
Fragment
>
{
getUploader
({
accept
:
'
video/*
'
})
}
</
Fragment
>
case
'
image
'
:
return
(
<
Fragment
>
{
getUploader
({
accept
:
'
image/*
'
})
}
</
Fragment
>
)
return
<
Fragment
>
{
getUploader
({
accept
:
'
image/*
'
})
}
</
Fragment
>
case
'
file
'
:
return
(
<
Fragment
>
{
getUploader
({
accept
:
'
*
'
,
afterParams
:
[
'
no_convert=1
'
]})
}
</
Fragment
>
<
Fragment
>
{
getUploader
({
accept
:
'
*
'
,
afterParams
:
[
'
no_convert=1
'
]
})
}
</
Fragment
>
)
case
'
voicemessage
'
:
return
(
<>
<
Fragment
>
{
isMobile
&&
<>
<
div
className
=
{
"
webwrap
"
}
>
<
div
>
Аудиозапись с мобильного устройства недоступна,
<
br
/>
запишите стандартными
функциями устройства и воспользуйтесь
кнопкой «Прикрепить файл»
{
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
"
}
/>
}
)
}
{
!
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
?
<
div
className
=
'audio-player-voice audio-player-margin'
/>
{
status
===
'
recording
'
&&
!
mediaBlobUrl
?
(
<
ReactStopwatch
seconds
=
{
0
}
minutes
=
{
0
}
hours
=
{
0
}
render
=
{
({
formatted
})
=>
{
render
=
{
({
formatted
})
=>
{
return
(
<
span
className
=
{
"
audio-player-timer audio-player-margin
"
}
>
{
formatted
}
</
span
>
<
span
className
=
'audio-player-timer audio-player-margin'
>
{
formatted
}
</
span
>
)
}
}
/>
:
<
span
className
=
{
"
audio-player-timer audio-player-margin
"
}
/>
}
/>
)
:
(
<
span
className
=
'audio-player-timer audio-player-margin'
/>
)
}
</
div
>
}
)
}
</
Fragment
>
</>
)
case
'
screencust
'
:
return
(
<>
<
Fragment
>
{
isMobile
&&
<
>
<
div
className
=
{
"
webwrap
"
}
>
<
div
>
Запись экрана с мобильного устройства недоступна,
<
br
/>
запишите
стандартными функциями устройства и воспользуйтесь
кнопкой «Загрузить видео»
{
isMobile
&&
(
<
div
className
=
'webwrap'
>
<
div
>
Запись экрана с мобильного устройства недоступна,
<
br
/
>
запишите стандартными функциями устройства и воспользуйтесь
кнопкой «Загрузить видео»
</
div
>
</
div
>
</>
}
{
!
isMobile
&&
)
}
{
!
isMobile
&&
(
<>
<
div
className
=
{
"
webwrap
"
}
>
<
div
className
=
{
"
webwrap-content
"
}
>
{
mediaBlobUrl
?
<
video
className
=
{
"
webwrap-video
"
}
id
=
{
"
id-video
"
}
src
=
{
mediaBlobUrl
}
controls
/>
:
status
===
"
recording
"
&&
<
video
className
=
{
"
webwrap-video
"
}
ref
=
{
videoRef
}
src
=
{
previewStream
}
autoPlay
controls
=
{
false
}
/>
}
{
status
===
'
recording
'
&&
!
mediaBlobUrl
?
<
div
className
=
'webwrap'
>
<
div
className
=
'webwrap-content'
>
{
mediaBlobUrl
?
(
<
video
className
=
'webwrap-video'
id
=
'id-video'
src
=
{
mediaBlobUrl
}
controls
/>
)
:
(
status
===
'
recording
'
&&
(
<
video
className
=
'webwrap-video'
ref
=
{
videoRef
}
src
=
{
previewStream
}
autoPlay
controls
=
{
false
}
/>
)
)
}
{
status
===
'
recording
'
&&
!
mediaBlobUrl
?
(
<
ReactStopwatch
seconds
=
{
0
}
minutes
=
{
0
}
hours
=
{
0
}
render
=
{
({
formatted
})
=>
{
render
=
{
({
formatted
})
=>
{
return
(
<
span
className
=
{
"
webwrap-timer
"
}
>
{
formatted
}
</
span
>
<
span
className
=
'webwrap-timer'
>
{
formatted
}
</
span
>
)
}
}
/>
:
<
span
className
=
{
"
webwrap-timer
"
}
>
00:00:00
</
span
>
}
{
!
mediaBlobUrl
&&
<
div
className
=
{
"
webwrap-start-border
"
}
>
/>
)
:
(
<
span
className
=
'webwrap-timer'
>
00:00:00
</
span
>
)
}
{
!
mediaBlobUrl
&&
(
<
div
className
=
'webwrap-start-border'
>
<
button
onClick
=
{
status
===
'
recording
'
?
stopRecording
:
startRecording
}
className
=
{
status
===
'
recording
'
?
"
webwrap-record-center
"
:
"
webwrap-start-center
"
}
/>
</
div
>
onClick
=
{
status
===
'
recording
'
?
stopRecording
:
startRecording
}
className
=
{
status
===
'
recording
'
?
'
webwrap-record-center
'
:
'
webwrap-start-center
'
}
/>
</
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
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
"
}
/>
}
{
!
mediaBlobUrl
&&
<
div
onClick
=
{
isAudioMuted
?
unMuteAudio
:
muteAudio
}
className
=
{
isAudioMuted
?
"
web-button-unmute
"
:
"
web-button-mute
"
}
/>
}
<
div
className
=
{
"
web-button-spacer
"
}
/>
/>
)
}
<
div
className
=
'web-button-spacer'
/>
</
div
>
</>
}
)
}
</
Fragment
>
</>
)
...
...
@@ -685,75 +740,91 @@ const QEditor = ({
return
(
<>
<
Fragment
>
{
isMobile
&&
<
>
<
div
className
=
{
"
webwrap
"
}
>
<
div
>
Видеозапись с мобильного устройства недоступна,
<
br
/>
запишите стандартными
функциями устройства и воспользуйтесь
кнопкой «Загрузить видео»
{
isMobile
&&
(
<
div
className
=
'webwrap'
>
<
div
>
Видеозапись с мобильного устройства недоступна,
<
br
/
>
запишите стандартными функциями устройства и воспользуйтесь
кнопкой «Загрузить видео»
</
div
>
</
div
>
</>
}
{
!
isMobile
&&
)
}
{
!
isMobile
&&
(
<>
<
div
className
=
{
"
webwrap
"
}
>
<
div
className
=
{
"
webwrap-content
"
}
>
{
mediaBlobUrl
?
<
video
className
=
{
"
webwrap-video
"
}
id
=
{
"
id-video
"
}
<
div
className
=
'webwrap'
>
<
div
className
=
'webwrap-content'
>
{
mediaBlobUrl
?
(
<
video
className
=
'webwrap-video'
id
=
'id-video'
src
=
{
mediaBlobUrl
}
controls
/>
:
status
===
"
recording
"
&&
<
video
className
=
{
"
webwrap-video
"
}
ref
=
{
videoRef
}
controls
/>
)
:
(
status
===
'
recording
'
&&
(
<
video
className
=
'webwrap-video'
ref
=
{
videoRef
}
src
=
{
previewStream
}
autoPlay
controls
=
{
false
}
/>
}
{
status
===
'
recording
'
&&
!
mediaBlobUrl
?
autoPlay
controls
=
{
false
}
/>
)
)
}
{
status
===
'
recording
'
&&
!
mediaBlobUrl
?
(
<
ReactStopwatch
seconds
=
{
0
}
minutes
=
{
0
}
hours
=
{
0
}
render
=
{
({
formatted
})
=>
{
render
=
{
({
formatted
})
=>
{
return
(
<
span
className
=
{
"
webwrap-timer
"
}
>
{
formatted
}
</
span
>
<
span
className
=
'webwrap-timer'
>
{
formatted
}
</
span
>
)
}
}
/>
:
<
span
className
=
{
"
webwrap-timer
"
}
>
00:00:00
</
span
>
}
{
!
mediaBlobUrl
&&
<
div
className
=
{
"
webwrap-start-border
"
}
>
/>
)
:
(
<
span
className
=
'webwrap-timer'
>
00:00:00
</
span
>
)
}
{
!
mediaBlobUrl
&&
(
<
div
className
=
'webwrap-start-border'
>
<
button
onClick
=
{
status
===
'
recording
'
?
stopRecording
:
startRecording
}
className
=
{
status
===
'
recording
'
?
"
webwrap-record-center
"
:
"
webwrap-start-center
"
}
/>
</
div
>
onClick
=
{
status
===
'
recording
'
?
stopRecording
:
startRecording
}
className
=
{
status
===
'
recording
'
?
'
webwrap-record-center
'
:
'
webwrap-start-center
'
}
/>
</
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
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
"
}
/>
}
{
!
mediaBlobUrl
&&
<
div
onClick
=
{
isAudioMuted
?
unMuteAudio
:
muteAudio
}
className
=
{
isAudioMuted
?
"
web-button-unmute
"
:
"
web-button-mute
"
}
/>
}
<
div
className
=
{
"
web-button-spacer
"
}
/>
/>
)
}
<
div
className
=
'web-button-spacer'
/>
</
div
>
</>
}
)
}
</
Fragment
>
</>
)
...
...
@@ -763,33 +834,33 @@ const QEditor = ({
}
const
isDisabledAction
=
()
=>
{
let
isDisabled
=
false
;
let
isDisabled
=
false
switch
(
innerModalType
)
{
case
'
video
'
:
case
'
image
'
:
if
(
uploadOptions
.
url
===
null
||
uploadedPaths
.
length
===
0
)
{
isDisabled
=
true
;
isDisabled
=
true
}
break
;
break
case
'
screencust
'
:
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
;
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
}
break
;
break
case
'
voicemessage
'
:
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
;
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
}
break
;
break
case
'
webcamera
'
:
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
;
if
(
status
===
'
recording
'
||
isUploading
||
!
mediaBlobUrl
)
{
isDisabled
=
true
}
break
;
break
case
'
iframe
'
:
try
{
le
t
url
=
new
URL
(
embedContent
)
;
cons
t
url
=
new
URL
(
embedContent
)
switch
(
url
.
hostname
)
{
case
'
rutube.ru
'
:
...
...
@@ -800,131 +871,93 @@ const QEditor = ({
case
'
youtu.be
'
:
case
'
youtube.com
'
:
case
'
www.youtube.com
'
:
break
;
break
default
:
isDisabled
=
true
;
isDisabled
=
true
}
}
catch
(
error
)
{
isDisabled
=
true
;
isDisabled
=
true
}
break
;
break
case
'
iframe_custom
'
:
let
regex
=
new
RegExp
(
'
(?:<iframe[^>]*)(?:(?:
\\
/>)|(?:>.*?<
\\
/iframe>))
'
);
isDisabled
=
!
regex
.
test
(
embedContent
);
break
;
const
regex
=
new
RegExp
(
'
(?:<iframe[^>]*)(?:(?:
\\
/>)|(?:>.*?<
\\
/iframe>))
'
)
isDisabled
=
!
regex
.
test
(
embedContent
)
break
}
return
isDisabled
;
return
isDisabled
}
if
(
!
editor
)
{
if
(
!
editor
)
{
return
null
}
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
()
}
>
const
buttons
=
innerModalType
===
'
remove_iframe
'
?
[
{
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
}
/>
)
}
})
title
:
'
Отмена
'
,
className
:
'
atma-editor-cancel
'
,
onClick
:
()
=>
{
stopRecording
()
unMuteAudio
()
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setUploadedPaths
([])
setModalIsOpen
(
false
)
}
</
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
:
'
Отмена
'
,
className
:
'
atma-editor-cancel
'
,
onClick
:
()
=>
{
stopRecording
()
;
unMuteAudio
()
;
clearBlobUrl
()
;
setUploaderUid
(
`uid
${
new
Date
()}
`
)
;
setUploadedPaths
([])
;
setModalIsOpen
(
false
)
;
stopRecording
()
unMuteAudio
()
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setUploadedPaths
([])
setModalIsOpen
(
false
)
}
},
{
title
:
(
mediaBlobUrl
&&
uploadedPaths
.
length
===
0
)
?
(
isUploading
?
'
Сохранение...
'
:
'
Вставить
'
)
:
'
Вставить
'
,
title
:
mediaBlobUrl
&&
uploadedPaths
.
length
===
0
?
isUploading
?
'
Сохранение...
'
:
'
Вставить
'
:
'
Вставить
'
,
className
:
'
atma-editor-complete
'
,
onClick
:
async
()
=>
{
if
((
status
===
'
recording
'
||
isUploading
))
{
return
false
;
if
(
status
===
'
recording
'
||
isUploading
)
{
return
false
}
else
{
if
(
document
.
querySelectorAll
(
'
.atma-editor-uploader-progress
'
).
length
>
0
)
{
if
(
!
confirm
(
'
Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?
'
))
{
return
false
;
if
(
document
.
querySelectorAll
(
'
.atma-editor-uploader-progress
'
)
.
length
>
0
)
{
if
(
// eslint-disable-next-line no-undef
!
confirm
(
'
Не полностью загруженные файлы будут утеряны. Вы уверены, что хотите продолжить?
'
)
)
{
return
false
}
}
...
...
@@ -932,135 +965,287 @@ const QEditor = ({
switch
(
innerModalType
)
{
case
'
image
'
:
uploadedPaths
.
map
((
file
,
i
)
=>
{
editor
.
chain
().
focus
().
setImage
({
src
:
file
.
path
})
.
run
();
})
;
editor
.
chain
().
focus
().
setImage
({
src
:
file
.
path
})
})
break
case
'
video
'
:
uploadedPaths
.
map
((
file
,
i
)
=>
{
editor
.
chain
().
focus
().
setVideo
({
editor
.
chain
()
.
focus
()
.
setVideo
({
src
:
file
.
path
,
poster
:
file
.
path
+
'
.jpg
'
}).
run
();
});
})
.
run
()
})
break
case
'
voicemessage
'
:
if
(
mediaBlobUrl
&&
uploadedPaths
.
length
===
0
)
{
if
(
!
isUploading
)
{
await
saveScreenCust
(
mediaBlobUrl
).
then
(
data
=>
{
if
(
!
isUploading
)
{
await
saveScreenCust
(
mediaBlobUrl
).
then
(
(
data
)
=>
{
if
(
data
?.
file_path
)
{
editor
.
chain
().
focus
().
addVoiceMessage
({
src
:
data
.
file_path
}).
run
();
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
(
!
isUploading
)
{
await
saveScreenCust
(
mediaBlobUrl
).
then
(
(
data
)
=>
{
if
(
data
?.
file_path
)
{
editor
.
chain
().
focus
().
setVideo
({
src
:
data
.
file_path
}).
run
();
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
(
!
isUploading
)
{
await
saveScreenCust
(
mediaBlobUrl
).
then
(
(
data
)
=>
{
if
(
data
?.
file_path
)
{
editor
.
chain
().
focus
().
setVideo
({
src
:
data
.
file_path
}).
run
();
editor
.
chain
()
.
focus
()
.
setVideo
({
src
:
data
.
file_path
})
.
run
()
}
})
;
})
}
}
break
case
'
iframe
'
:
let
_url
=
embedContent
;
le
t
reg
=
/
(
http|https
)
:
\/\/([\w
.
]
+
\/?)\S
*/
;
let
_url
=
embedContent
cons
t
reg
=
/
(
http|https
)
:
\/\/([\w
.
]
+
\/?)\S
*/
const
url
=
new
URL
(
reg
.
test
(
_url
)
?
_url
:
'
https:
'
+
_url
);
let
urlId
=
url
.
pathname
.
replace
(
/
\/
$/ig
,
''
).
split
(
'
/
'
).
pop
();
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
}
`
;
_url
=
`https://rutube.ru/pl/?pl_id&pl_type&pl_video=
${
urlId
}
`
break
case
'
vimeo.com
'
:
_url
=
`https://player.vimeo.com/video/
${
urlId
}
`
;
_url
=
`https://player.vimeo.com/video/
${
urlId
}
`
break
case
'
ok.ru
'
:
case
'
www.ok.ru
'
:
_url
=
`//ok.ru/videoembed/
${
urlId
}
`
;
_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
.
hostname
.
indexOf
(
'
youtu.be
'
)
===
-
1
&&
url
.
search
!==
''
)
{
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
}
editor
.
chain
().
focus
().
setIframe
({
src
:
_url
}).
run
();
editor
.
chain
().
focus
().
setIframe
({
src
:
_url
}).
run
()
break
case
'
iframe_custom
'
:
editor
.
chain
().
focus
().
insertContent
(
embedContent
).
run
()
;
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
'
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
();
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
()
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
'
:
uploadedPaths
.
map
((
file
,
i
)
=>
{
let
exp
=
file
.
path
.
split
(
'
.
'
)
;
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>`
).
run
();
});
editor
.
chain
()
.
focus
()
.
insertContent
(
`
<a href="
${
file
.
path
}
" target="_blank" download="
${
file
.
name
}
.
${
exp
}
" data-size="
${
file
.
size
}
">
${
file
.
name
}
</a>
`
)
.
run
()
})
break
}
setModalIsOpen
(
false
);
clearBlobUrl
();
setUploaderUid
(
`uid
${
new
Date
()}
`
);
setEmbedContent
(
''
);
setUploadedPaths
([]);
setModalTitle
(
''
);
setModalIsOpen
(
false
)
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setEmbedContent
(
''
)
setUploadedPaths
([])
setModalTitle
(
''
)
}
catch
(
err
)
{
console
.
log
(
err
)
;
setModalIsOpen
(
false
)
;
clearBlobUrl
()
;
setUploaderUid
(
`uid
${
new
Date
()}
`
)
;
setEmbedContent
(
''
)
;
setUploadedPaths
([])
;
setModalTitle
(
''
)
;
console
.
log
(
err
)
setModalIsOpen
(
false
)
clearBlobUrl
()
setUploaderUid
(
`uid
${
new
Date
()}
`
)
setEmbedContent
(
''
)
setUploadedPaths
([])
setModalTitle
(
''
)
}
}
},
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
>
</
div
>
)
}
export
default
QEditor
;
export
default
QEditor
src/extensions/Iframe.js
View file @
cbdf4e4b
...
...
@@ -9,68 +9,76 @@ const Iframe = Node.create({
addAttributes
()
{
return
{
"
src
"
:
{
src
:
{
default
:
null
},
"
frameborder
"
:
{
default
:
0
,
frameborder
:
{
default
:
0
},
"
allowfullscreen
"
:
{
allowfullscreen
:
{
default
:
true
,
parseHTML
:
()
=>
{
console
.
log
(
this
)
}
,
}
,
}
}
}
},
parseHTML
()
{
return
[
{
tag
:
'
iframe
'
,
}
,
tag
:
'
iframe
'
}
]
},
renderHTML
({
HTMLAttributes
})
{
return
[
'
iframe
'
,
mergeAttributes
(
HTMLAttributes
)]
;
return
[
'
iframe
'
,
mergeAttributes
(
HTMLAttributes
)]
},
addNodeView
()
{
return
({
editor
,
node
,
...
a
})
=>
{
const
container
=
document
.
createElement
(
'
div
'
)
const
iframe
=
document
.
createElement
(
'
iframe
'
)
iframe
.
src
=
node
.
attrs
.
src
iframe
.
allowfullscreen
=
node
.
attrs
.
allowfullscreen
iframe
.
classList
.
add
(
'
customIframe
'
)
// div.className = 'aspect-w-16 aspect-h-9' + (editor.isEditable ? ' cursor-pointer' : '');
const
iframe
=
document
.
createElement
(
'
iframe
'
);
if
(
editor
.
isEditable
)
{
iframe
.
className
=
'
pointer-events-none
'
;
}
const
closeBtn
=
document
.
createElement
(
'
button
'
)
closeBtn
.
textContent
=
'
X
'
closeBtn
.
classList
.
add
(
'
closeBtn
'
)
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
{
dom
:
iframe
,
dom
:
container
}
}
},
addCommands
()
{
return
{
setIframe
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
setIframe
:
(
options
)
=>
({
tr
,
dispatch
})
=>
{
const
{
selection
}
=
tr
const
node
=
this
.
type
.
create
(
options
)
//
if
(
dispatch
)
{
tr
.
replaceRangeWith
(
selection
.
from
,
selection
.
to
,
node
)
}
return
true
},
}
},
});
}
}
})
export
default
Iframe
;
export
default
Iframe
src/index.scss
View file @
cbdf4e4b
...
...
@@ -1045,4 +1045,25 @@ body{
.qseparator{
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;
}
}
src/modals/IframeCustomModal.js
0 → 100644
View file @
cbdf4e4b
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
>
)
}
src/modals/IframeModal.js
0 → 100644
View file @
cbdf4e4b
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
>
)
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment