Commit 76fb932d by Яков

fix crash mobile

parent b68463ab
{
"name": "react-ag-qeditor",
"version": "1.0.17",
"version": "1.0.20",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......@@ -74,6 +74,7 @@
"prosemirror-state": "1.4.0",
"rc-upload": "^4.3.3",
"react": "^17.0.2",
"react-device-detect": "^2.2.3",
"react-media-recorder": "^1.6.6",
"react-stopwatch": "^2.0.4",
"react-webcam": "^7.0.1",
......
......@@ -30,9 +30,17 @@ import axios from "axios";
import ReactStopwatch from 'react-stopwatch';
import Audio from "./extensions/Audio";
import { isMobile } from 'react-device-detect';
const initialBubbleItems = ['bold', 'italic', 'underline', 'strike', '|', 'colorText', 'highlight'];
const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", errorMessage: ""}, toolsOptions = { type: 'all' } }) => {
const QEditor = ({
value,
onChange = () => {},
style,
uploadOptions = {url: "", errorMessage: ""},
toolsOptions = {type: 'all'}
}) => {
global.uploadUrl = uploadOptions.url;
const [innerModalType, setInnerModalType] = useState(null);
......@@ -46,13 +54,23 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
const [focusFromTo, setFocusFromTo] = useState(null);
const [oldFocusFromTo, setOldFocusFromTo] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const [recordType, setRecordType] = useState({screen: true})
const [recordType, setRecordType] = useState({video: true})
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;
}
const { status, startRecording, stopRecording, mediaBlobUrl, previewStream, muteAudio, unMuteAudio, isAudioMuted, clearBlobUrl } = useReactMediaRecorder(recordType);
const {
status,
startRecording,
stopRecording,
mediaBlobUrl,
previewStream,
muteAudio,
unMuteAudio,
isAudioMuted,
clearBlobUrl
} = useReactMediaRecorder(recordType);
const videoRef = useRef(null);
......@@ -63,7 +81,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
}, [previewStream]);
useEffect(() => {
if(focusFromTo !== oldFocusFromTo){
if (focusFromTo !== oldFocusFromTo) {
setColorsSelected(null)
setOldFocusFromTo(focusFromTo);
}
......@@ -121,15 +139,13 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
// 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: {
......@@ -150,15 +166,15 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
},
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: 'Обычный',
......@@ -239,7 +255,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
},
insertTable: {
title: 'Вставить таблицу',
onClick: () => editor.chain().focus().insertTable({ rows: 2, cols: 2 }).run()
onClick: () => editor.chain().focus().insertTable({rows: 2, cols: 2}).run()
},
deleteTable: {
title: 'Удалить таблицу',
......@@ -307,7 +323,11 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
screencust: {
title: 'Записать экран',
onClick: () => {
setRecordType({screen: true})
if (isMobile) {
setRecordType({video: true})
} else {
setRecordType({screen: true})
}
clearBlobUrl()
modalOpener('screencust', 'Записать экран')
}
......@@ -372,10 +392,10 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
],
content: value,
onUpdate: ({editor}) => onChange(editor.getHTML()),
onFocus: ({editor})=>{
onFocus: ({editor}) => {
let wrap = editor.options.element.closest('.atma-editor-wrap');
wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function(s) {
wrap.querySelectorAll('.atma-editor-toolbar-s').forEach(function (s) {
s.classList.remove('show');
});
......@@ -391,7 +411,8 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
<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}
<button disabled={btn.disabled} type={'button'} key={'mAction' + i}
className={'atma-editor-btn' + btn.className}
onClick={btn.onClick}>{btn.title}</button>
))
}
......@@ -399,34 +420,34 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
)
}
const getUploader = ({ accept = '*', ...o }) => {
const getUploader = ({accept = '*', ...o}) => {
let url = uploadOptions.url;
if(o.afterParams && o.afterParams.length > 0){
if(uploadOptions.url.indexOf('?') !== -1){
if (o.afterParams && o.afterParams.length > 0) {
if (uploadOptions.url.indexOf('?') !== -1) {
url = uploadOptions.url + '&' + o.afterParams.join('&');
}else{
} else {
url = uploadOptions.url + '?' + o.afterParams.join('&');
}
}
return <Uploader
key={uploaderUid}
accept={ accept }
action={ url }
errorMessage={ uploadOptions.errorMessage }
accept={accept}
action={url}
errorMessage={uploadOptions.errorMessage}
onSuccess={(file) => {
let _uploadedPaths = [...uploadedPaths];
_uploadedPaths.push(file);
setUploadedPaths(_uploadedPaths)
}}
onDelete={(deleteFile)=>{
onDelete={(deleteFile) => {
let deleteIdx = null;
let _uploadedPaths = [...uploadedPaths];
_uploadedPaths.map((f, i)=>{
if(f.uid === deleteFile.uid){
_uploadedPaths.map((f, i) => {
if (f.uid === deleteFile.uid) {
deleteIdx = i;
}
});
......@@ -434,7 +455,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
setUploadedPaths(_uploadedPaths)
}}
multiple={true}
modalType={ innerModalType }
modalType={innerModalType}
/>
}
......@@ -465,8 +486,9 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
case 'iframe':
return (
<Fragment>
<input type="text" value={embedContent} placeholder={'https://'} onInput={(e) => setEmbedContent(e.target.value)
}/>
<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'}/>
......@@ -478,43 +500,58 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
)
case 'video':
return (
<Fragment>{ getUploader({ accept: 'video/*' }) }</Fragment>
<Fragment>{getUploader({accept: 'video/*'})}</Fragment>
)
case 'image':
return (
<Fragment>{ getUploader({ accept: 'image/*' }) }</Fragment>
<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>
<div className={"audio-player"}>
<div className={"audio-player-start audio-player-margin"}>
{
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 ?
<div onClick={stopRecording} className={"audio-player-center-recording"}/> :
<div onClick={startRecording} className={"audio-player-center-start"}/>
status === 'recording' && ! mediaBlobUrl ?
<ReactStopwatch
seconds={0}
minutes={0}
hours={0}
render={({formatted}) => {
return (
<span
className={"audio-player-timer audio-player-margin"}>{formatted}</span>
)
}}
/> : <span className={"audio-player-timer audio-player-margin"}/>
}
</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}</span>
)
}}
/> : <span className={"audio-player-timer audio-player-margin"} />
}
</div>
}
</Fragment>
</>
)
......@@ -522,110 +559,152 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
return (
<>
<Fragment>
<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 }) => {
return (
<span className={"webwrap-timer"}>
{
isMobile &&
<>
<div className={"webwrap"}>
<div>Запись экрана с мобильного устройства недоступна, <br/> запишите
стандартными функциями устройства и воспользуйтесь кнопкой «Загрузить видео»
</div>
</div>
</>
}
{
! 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 ?
<ReactStopwatch
seconds={0}
minutes={0}
hours={0}
render={({formatted}) => {
return (
<span className={"webwrap-timer"}>
{formatted}
</span>
)
}}
/> : <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"} />
)
}}
/> : <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>
}
</div>
}
</div>
</div>
<div className={"web-bottom-elements"}>
{ mediaBlobUrl &&
<div onClick={clearBlobUrl} className={"web-button-wrap"}>
<div className={"web-button-rerecord"}/>
<span className={"web-button-rerecord-text"}>Перезаписать</span>
</div>
}
{
!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>
<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"}/>
}
<div className={"web-button-spacer"}/>
</div>
</>
}
</Fragment>
</>
)
case 'webcamera':
return (
<>
<Fragment>
<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 }) => {
return (
<span className={"webwrap-timer"}>
{formatted}
</span>
)
}}
/> : <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>
}
</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>
}
<Fragment>
{
!mediaBlobUrl &&
<div className={"web-button-spacer"}/>
isMobile &&
<>
<div className={"webwrap"}>
<div>Видеозапись с мобильного устройства недоступна, <br/> запишите стандартными
функциями устройства и воспользуйтесь кнопкой «Загрузить видео»
</div>
</div>
</>
}
{
!mediaBlobUrl &&
<div onClick={isAudioMuted ? unMuteAudio : muteAudio} className={isAudioMuted ? "web-button-unmute" : "web-button-mute"}/>
! 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 ?
<ReactStopwatch
seconds={0}
minutes={0}
hours={0}
render={({formatted}) => {
return (
<span className={"webwrap-timer"}>
{formatted}
</span>
)
}}
/> : <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>
}
</div>
</div>
<div className={"web-bottom-elements"}>
{mediaBlobUrl &&
<div onClick={clearBlobUrl} className={"web-button-wrap"}>
<div className={"web-button-rerecord"}/>
<span className={"web-button-rerecord-text"}>Перезаписать</span>
</div>
}
{
! mediaBlobUrl &&
<div className={"web-button-spacer"}/>
}
{
! mediaBlobUrl &&
<div onClick={isAudioMuted ? unMuteAudio : muteAudio}
className={isAudioMuted ? "web-button-unmute" : "web-button-mute"}/>
}
<div className={"web-button-spacer"}/>
</div>
</>
}
<div className={"web-button-spacer"}/>
</div>
</Fragment>
</>
)
</Fragment>
</>
)
default:
return <div>Пусто</div>
}
......@@ -634,33 +713,33 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
const isDisabledAction = () => {
let isDisabled = false;
switch(innerModalType){
switch (innerModalType) {
case 'video':
case 'image':
if(uploadOptions.url === null || uploadedPaths.length === 0){
if (uploadOptions.url === null || uploadedPaths.length === 0) {
isDisabled = true;
}
break;
case 'screencust':
if(status === 'recording' || isUploading || !mediaBlobUrl){
if (status === 'recording' || isUploading || ! mediaBlobUrl) {
isDisabled = true;
}
break;
case 'voicemessage':
if(status === 'recording' || isUploading || !mediaBlobUrl){
if (status === 'recording' || isUploading || ! mediaBlobUrl) {
isDisabled = true;
}
break;
case 'webcamera':
if(status === 'recording' || isUploading || !mediaBlobUrl){
if (status === 'recording' || isUploading || ! mediaBlobUrl) {
isDisabled = true;
}
break;
case 'iframe':
try{
try {
let url = new URL(embedContent);
switch (url.hostname){
switch (url.hostname) {
case 'rutube.ru':
case 'www.rutube.ru':
case 'vimeo.com':
......@@ -674,7 +753,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
isDisabled = true;
}
}catch (error){
} catch (error) {
isDisabled = true;
}
break;
......@@ -683,7 +762,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
return isDisabled;
}
if (!editor) {
if ( ! editor) {
return null
}
......@@ -702,48 +781,50 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
<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){
if (o.from !== o.to && editor.isActive('paragraph') && editor.isActive('image') === false && document.querySelectorAll('.selectedCell').length === 0) {
items = initialBubbleItems;
}
if(editor.isActive('image') === true){
if (editor.isActive('image') === true) {
items = ['alignLeft', 'alignCenter', 'alignRight'];
}
setFocusFromTo([o.from, o.to].join(':'));
if(items.length > 0){
if (items.length > 0) {
setBubbleItems(items);
return true;
}
}} tippyOptions={{ duration: 100 }}>
}} 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={()=>{
return (<div key={'colors' + colorsSelected + i}
className={'qcolors' + (itemColor === 'none' ? ' unset' : '')}
style={{background: itemColor}} onClick={() => {
if(itemColor === 'none'){
if (itemColor === 'none') {
colorsSelected === 'color' ?
editor.chain().focus().unsetHighlight().unsetColor().run() :
editor.chain().focus().unsetColor().unsetHighlight().run();
}else{
} else {
colorsSelected === 'color' ?
editor.chain().focus().unsetHighlight().setColor(itemColor).run() :
editor.chain().focus().unsetColor().toggleHighlight({ color: 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{
if (type === '|') {
return (<div key={'bubbleSeparator' + i} className={'qseparator'}/>)
} else {
return (
<div
key={ 'bubbleItems' + i }
key={'bubbleItems' + i}
className={'qicon q' + type + (editor.isActive(type) ? ' active' : '')}
title={ toolsLib[type] ? toolsLib[type].title : '' }
onClick={ toolsLib[type].onClick }
title={toolsLib[type] ? toolsLib[type].title : ''}
onClick={toolsLib[type].onClick}
/>
)
}
......@@ -876,8 +957,7 @@ const QEditor = ({ value, onChange = ()=>{}, style, uploadOptions = {url: "", er
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>`).run();
editor.chain().focus().insertContent(`<a href="${file.path}" target="_blank" download="${file.name}.${exp}" data-size="${file.size}">${file.name}</a>`).run();
});
break
}
......
......@@ -10193,6 +10193,13 @@ react-dev-utils@^10.2.1:
strip-ansi "6.0.0"
text-table "0.2.0"
react-device-detect@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca"
integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==
dependencies:
ua-parser-js "^1.0.33"
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
......@@ -12054,6 +12061,11 @@ typescript@^3.8.3:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
ua-parser-js@^1.0.33:
version "1.0.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4"
integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz"
......
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