1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

Add image embed in the text editor

This commit is contained in:
vincent 2022-03-28 14:01:14 +02:00 committed by Sylvain
parent cd5c253b3e
commit 67f678a282
5 changed files with 87 additions and 33 deletions

View File

@ -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']));

View File

@ -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,17 +38,19 @@ 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);
const previousUrl = { if (type === 'link') {
href: editor.getAttributes('link').href, const previousUrl = {
target: editor.getAttributes('link').target || '' href: editor.getAttributes('link').href,
}; target: editor.getAttributes('link').target || ''
// display selected text's attributes if it's a link };
if (previousUrl.href) { // display selected text's attributes if it's a link
setUrl(previousUrl); if (previousUrl.href) {
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>
</> </>
); );

View File

@ -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;

View File

@ -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",

View File

@ -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"