mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-19 08:52:25 +01:00
Add image embed in the text editor
This commit is contained in:
parent
cd5c253b3e
commit
67f678a282
@ -10,6 +10,7 @@ import CharacterCount from '@tiptap/extension-character-count';
|
|||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import Link from '@tiptap/extension-link';
|
import Link from '@tiptap/extension-link';
|
||||||
import Iframe from './iframe';
|
import Iframe from './iframe';
|
||||||
|
import Image from '@tiptap/extension-image';
|
||||||
import { MenuBar } from './menu-bar';
|
import { MenuBar } from './menu-bar';
|
||||||
import { WarningOctagon } from 'phosphor-react';
|
import { WarningOctagon } from 'phosphor-react';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ interface FabTextEditorProps {
|
|||||||
content?: string,
|
content?: string,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
video?: boolean,
|
video?: boolean,
|
||||||
|
image?: boolean,
|
||||||
onChange?: (content: string) => void,
|
onChange?: (content: string) => void,
|
||||||
placeholder?: string,
|
placeholder?: string,
|
||||||
error?: string
|
error?: string
|
||||||
@ -29,7 +31,7 @@ interface FabTextEditorProps {
|
|||||||
/**
|
/**
|
||||||
* This component is a WYSIWYG text editor
|
* This component is a WYSIWYG text editor
|
||||||
*/
|
*/
|
||||||
export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit = 400, video, onChange, placeholder, error }) => {
|
export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit = 400, video, image, onChange, placeholder, error }) => {
|
||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
const placeholderText = placeholder || t('app.shared.text_editor.placeholder');
|
const placeholderText = placeholder || t('app.shared.text_editor.placeholder');
|
||||||
// TODO: Add ctrl+click on link to visit
|
// TODO: Add ctrl+click on link to visit
|
||||||
@ -54,7 +56,12 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
|
|||||||
CharacterCount.configure({
|
CharacterCount.configure({
|
||||||
limit
|
limit
|
||||||
}),
|
}),
|
||||||
Iframe
|
Iframe,
|
||||||
|
Image.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'fab-textEditor-image'
|
||||||
|
}
|
||||||
|
})
|
||||||
],
|
],
|
||||||
content,
|
content,
|
||||||
onUpdate: ({ editor }) => {
|
onUpdate: ({ editor }) => {
|
||||||
@ -70,7 +77,7 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
|
|||||||
<>
|
<>
|
||||||
{label && <label onClick={focusEditor} className="fab-textEditor-label">{label}</label>}
|
{label && <label onClick={focusEditor} className="fab-textEditor-label">{label}</label>}
|
||||||
<div className="fab-textEditor">
|
<div className="fab-textEditor">
|
||||||
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} />
|
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} image={image} />
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
<div className="fab-textEditor-character-count">
|
<div className="fab-textEditor-character-count">
|
||||||
{editor?.storage.characterCount.characters()} / {limit}
|
{editor?.storage.characterCount.characters()} / {limit}
|
||||||
@ -86,12 +93,12 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FabTextEditorWrapper: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit, video, placeholder, error }) => {
|
const FabTextEditorWrapper: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit, video, image, placeholder, error }) => {
|
||||||
return (
|
return (
|
||||||
<Loader>
|
<Loader>
|
||||||
<FabTextEditor label={label} paragraphTools={paragraphTools} content={content} limit={limit} video={video} placeholder={placeholder} error={error} />
|
<FabTextEditor label={label} paragraphTools={paragraphTools} content={content} limit={limit} video={video} image={image} placeholder={placeholder} error={error} />
|
||||||
</Loader>
|
</Loader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Application.Components.component('fabTextEditor', react2angular(FabTextEditorWrapper, ['label', 'paragraphTools', 'content', 'limit', 'video', 'placeholder', 'error']));
|
Application.Components.component('fabTextEditor', react2angular(FabTextEditorWrapper, ['label', 'paragraphTools', 'content', 'limit', 'video', 'image', 'placeholder', 'error']));
|
||||||
|
@ -2,18 +2,19 @@ import React, { useCallback, useState, useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useOnclickOutside from 'react-cool-onclickoutside';
|
import useOnclickOutside from 'react-cool-onclickoutside';
|
||||||
import { Editor } from '@tiptap/react';
|
import { Editor } from '@tiptap/react';
|
||||||
import { TextAa, TextBolder, TextItalic, TextUnderline, LinkSimpleHorizontal, ListBullets, Quotes, Trash, CheckCircle, VideoCamera } from 'phosphor-react';
|
import { TextAa, TextBolder, TextItalic, TextUnderline, LinkSimpleHorizontal, ListBullets, Quotes, Trash, CheckCircle, VideoCamera, Image } from 'phosphor-react';
|
||||||
|
|
||||||
interface MenuBarProps {
|
interface MenuBarProps {
|
||||||
|
editor?: Editor,
|
||||||
paragraphTools?: boolean,
|
paragraphTools?: boolean,
|
||||||
video?: boolean,
|
video?: boolean,
|
||||||
editor?: Editor,
|
image?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component is the menu bar for the WYSIWYG text editor
|
* This component is the menu bar for the WYSIWYG text editor
|
||||||
*/
|
*/
|
||||||
export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video }) => {
|
export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video, image }) => {
|
||||||
const { t } = useTranslation('shared');
|
const { t } = useTranslation('shared');
|
||||||
|
|
||||||
const [submenu, setSubmenu] = useState('');
|
const [submenu, setSubmenu] = useState('');
|
||||||
@ -21,12 +22,14 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
const [url, setUrl] = useState(resetUrl);
|
const [url, setUrl] = useState(resetUrl);
|
||||||
const [videoProvider, setVideoProvider] = useState('youtube');
|
const [videoProvider, setVideoProvider] = useState('youtube');
|
||||||
const [videoId, setVideoId] = useState('');
|
const [videoId, setVideoId] = useState('');
|
||||||
|
const [imageUrl, setImageUrl] = useState('');
|
||||||
|
|
||||||
// Reset state values when the submenu is closed
|
// Reset state values when the submenu is closed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!submenu) {
|
if (!submenu) {
|
||||||
setUrl(resetUrl);
|
setUrl(resetUrl);
|
||||||
setVideoProvider('youtube');
|
setVideoProvider('youtube');
|
||||||
|
setImageUrl('');
|
||||||
}
|
}
|
||||||
}, [submenu]);
|
}, [submenu]);
|
||||||
|
|
||||||
@ -35,10 +38,11 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
setSubmenu('');
|
setSubmenu('');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle link menu's visibility
|
// Toggle submenu's visibility
|
||||||
const toggleLinkMenu = () => {
|
const toggleSubmenu = (type) => {
|
||||||
if (submenu !== 'link') {
|
if (submenu !== type) {
|
||||||
setSubmenu('link');
|
setSubmenu(type);
|
||||||
|
if (type === 'link') {
|
||||||
const previousUrl = {
|
const previousUrl = {
|
||||||
href: editor.getAttributes('link').href,
|
href: editor.getAttributes('link').href,
|
||||||
target: editor.getAttributes('link').target || ''
|
target: editor.getAttributes('link').target || ''
|
||||||
@ -47,6 +51,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
if (previousUrl.href) {
|
if (previousUrl.href) {
|
||||||
setUrl(previousUrl);
|
setUrl(previousUrl);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setSubmenu('');
|
setSubmenu('');
|
||||||
}
|
}
|
||||||
@ -88,21 +93,12 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
setSubmenu('');
|
setSubmenu('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Toggle video menu's visibility
|
|
||||||
const toggleVideoMenu = () => {
|
|
||||||
if (submenu !== 'video') {
|
|
||||||
setSubmenu('video');
|
|
||||||
} else {
|
|
||||||
setSubmenu('');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Store selected video provider in state
|
// Store selected video provider in state
|
||||||
const handleSelect = (evt) => {
|
const handleSelect = (evt) => {
|
||||||
setVideoProvider(evt.target.value);
|
setVideoProvider(evt.target.value);
|
||||||
};
|
};
|
||||||
// Store video id in state
|
// Store video id in state
|
||||||
const VideoUrlChange = (evt) => {
|
const videoUrlChange = (evt) => {
|
||||||
const id = evt.target.value.match(/([^/]+$)/g);
|
const id = evt.target.value.match(/([^/]+$)/g);
|
||||||
setVideoId(id);
|
setVideoId(id);
|
||||||
};
|
};
|
||||||
@ -126,6 +122,18 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
setSubmenu('');
|
setSubmenu('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store image url in state
|
||||||
|
const imageUrlChange = (evt) => {
|
||||||
|
setImageUrl(evt.target.value);
|
||||||
|
};
|
||||||
|
// Insert image
|
||||||
|
const addImage = () => {
|
||||||
|
if (imageUrl) {
|
||||||
|
editor.chain().focus().setImage({ src: imageUrl }).run();
|
||||||
|
setSubmenu('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -182,22 +190,34 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={toggleLinkMenu}
|
onClick={() => toggleSubmenu('link')}
|
||||||
className={`ignore-onclickoutside ${editor.isActive('link') ? 'is-active' : ''}`}
|
className={`ignore-onclickoutside ${editor.isActive('link') ? 'is-active' : ''}`}
|
||||||
>
|
>
|
||||||
<LinkSimpleHorizontal size={24} />
|
<LinkSimpleHorizontal size={24} />
|
||||||
</button>
|
</button>
|
||||||
|
{ (video || image) && <span className='divider'></span> }
|
||||||
{ video &&
|
{ video &&
|
||||||
(<>
|
(<>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
onClick={toggleVideoMenu}
|
onClick={() => toggleSubmenu('video')}
|
||||||
>
|
>
|
||||||
<VideoCamera size={24} />
|
<VideoCamera size={24} />
|
||||||
</button>
|
</button>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
{ image &&
|
||||||
|
(<>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={() => toggleSubmenu('image')}
|
||||||
|
>
|
||||||
|
<Image size={24} />
|
||||||
|
</button>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
|
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
|
||||||
{ submenu === 'link' &&
|
{ submenu === 'link' &&
|
||||||
(<>
|
(<>
|
||||||
@ -227,13 +247,23 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
|
|||||||
<option value="dailymotion">Dailymotion</option>
|
<option value="dailymotion">Dailymotion</option>
|
||||||
</select>
|
</select>
|
||||||
<div>
|
<div>
|
||||||
<input type="text" onChange={VideoUrlChange} placeholder={t('app.shared.text_editor.link_placeholder')} />
|
<input type="text" onChange={videoUrlChange} placeholder={t('app.shared.text_editor.link_placeholder')} />
|
||||||
<button type='button' onClick={() => addIframe()}>
|
<button type='button' onClick={() => addIframe()}>
|
||||||
<CheckCircle size={24} />
|
<CheckCircle size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
{ submenu === 'image' &&
|
||||||
|
(<>
|
||||||
|
<div>
|
||||||
|
<input type="text" onChange={imageUrlChange} placeholder={t('app.shared.text_editor.link_placeholder')} />
|
||||||
|
<button type='button' onClick={() => addImage()}>
|
||||||
|
<CheckCircle size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -157,6 +157,7 @@
|
|||||||
height: 0;
|
height: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
margin: 1rem 0;
|
||||||
padding-bottom: calc(100% / 16 * 9);
|
padding-bottom: calc(100% / 16 * 9);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
@ -167,6 +168,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-image {
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: min(75vh, 600px);
|
||||||
|
margin: 1rem 0;
|
||||||
|
&.ProseMirror-selectednode {
|
||||||
|
box-shadow: 0 0 0 2px var(--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-error {
|
&-error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4.5rem;
|
top: 4.5rem;
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
"@stripe/stripe-js": "^1.13.2",
|
"@stripe/stripe-js": "^1.13.2",
|
||||||
"@tiptap/core": "^2.0.0-beta.174",
|
"@tiptap/core": "^2.0.0-beta.174",
|
||||||
"@tiptap/extension-character-count": "^2.0.0-beta.24",
|
"@tiptap/extension-character-count": "^2.0.0-beta.24",
|
||||||
|
"@tiptap/extension-image": "^2.0.0-beta.27",
|
||||||
"@tiptap/extension-link": "^2.0.0-beta.36",
|
"@tiptap/extension-link": "^2.0.0-beta.36",
|
||||||
"@tiptap/extension-placeholder": "^2.0.0-beta.47",
|
"@tiptap/extension-placeholder": "^2.0.0-beta.47",
|
||||||
"@tiptap/extension-underline": "^2.0.0-beta.22",
|
"@tiptap/extension-underline": "^2.0.0-beta.22",
|
||||||
|
@ -1534,6 +1534,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-state "^1.3.4"
|
prosemirror-state "^1.3.4"
|
||||||
|
|
||||||
|
"@tiptap/extension-image@^2.0.0-beta.27":
|
||||||
|
version "2.0.0-beta.27"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.0.0-beta.27.tgz#62152240cfa7ead03080c38485c1ebda4a603d18"
|
||||||
|
integrity sha512-kdJ7V39yNdVWUco/RBe7WgvFevd81l+pU6+Je9HpelqBBP953wDttzLMuAWQB4AeLv9WhKSlORHiFv2SKsV5NA==
|
||||||
|
|
||||||
"@tiptap/extension-italic@^2.0.0-beta.26":
|
"@tiptap/extension-italic@^2.0.0-beta.26":
|
||||||
version "2.0.0-beta.26"
|
version "2.0.0-beta.26"
|
||||||
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.26.tgz#b00c9e32b81b1bd94eaed24bb2a22e44d5dc54a3"
|
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.26.tgz#b00c9e32b81b1bd94eaed24bb2a22e44d5dc54a3"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user