1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +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 4c6a1fb0fc
commit d1daa55be1
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 Link from '@tiptap/extension-link';
import Iframe from './iframe';
import Image from '@tiptap/extension-image';
import { MenuBar } from './menu-bar';
import { WarningOctagon } from 'phosphor-react';
@ -21,6 +22,7 @@ interface FabTextEditorProps {
content?: string,
limit?: number,
video?: boolean,
image?: boolean,
onChange?: (content: string) => void,
placeholder?: string,
error?: string
@ -29,7 +31,7 @@ interface FabTextEditorProps {
/**
* 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 placeholderText = placeholder || t('app.shared.text_editor.placeholder');
// TODO: Add ctrl+click on link to visit
@ -54,7 +56,12 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
CharacterCount.configure({
limit
}),
Iframe
Iframe,
Image.configure({
HTMLAttributes: {
class: 'fab-textEditor-image'
}
})
],
content,
onUpdate: ({ editor }) => {
@ -70,7 +77,7 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
<>
{label && <label onClick={focusEditor} className="fab-textEditor-label">{label}</label>}
<div className="fab-textEditor">
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} />
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} image={image} />
<EditorContent editor={editor} />
<div className="fab-textEditor-character-count">
{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 (
<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>
);
};
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 useOnclickOutside from 'react-cool-onclickoutside';
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 {
editor?: Editor,
paragraphTools?: boolean,
video?: boolean,
editor?: Editor,
image?: boolean,
}
/**
* 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 [submenu, setSubmenu] = useState('');
@ -21,12 +22,14 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
const [url, setUrl] = useState(resetUrl);
const [videoProvider, setVideoProvider] = useState('youtube');
const [videoId, setVideoId] = useState('');
const [imageUrl, setImageUrl] = useState('');
// Reset state values when the submenu is closed
useEffect(() => {
if (!submenu) {
setUrl(resetUrl);
setVideoProvider('youtube');
setImageUrl('');
}
}, [submenu]);
@ -35,17 +38,19 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
setSubmenu('');
});
// Toggle link menu's visibility
const toggleLinkMenu = () => {
if (submenu !== 'link') {
setSubmenu('link');
const previousUrl = {
href: editor.getAttributes('link').href,
target: editor.getAttributes('link').target || ''
};
// display selected text's attributes if it's a link
if (previousUrl.href) {
setUrl(previousUrl);
// Toggle submenu's visibility
const toggleSubmenu = (type) => {
if (submenu !== type) {
setSubmenu(type);
if (type === 'link') {
const previousUrl = {
href: editor.getAttributes('link').href,
target: editor.getAttributes('link').target || ''
};
// display selected text's attributes if it's a link
if (previousUrl.href) {
setUrl(previousUrl);
}
}
} else {
setSubmenu('');
@ -88,21 +93,12 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
setSubmenu('');
};
// Toggle video menu's visibility
const toggleVideoMenu = () => {
if (submenu !== 'video') {
setSubmenu('video');
} else {
setSubmenu('');
}
};
// Store selected video provider in state
const handleSelect = (evt) => {
setVideoProvider(evt.target.value);
};
// Store video id in state
const VideoUrlChange = (evt) => {
const videoUrlChange = (evt) => {
const id = evt.target.value.match(/([^/]+$)/g);
setVideoId(id);
};
@ -126,6 +122,18 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
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) {
return null;
}
@ -182,22 +190,34 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
</button>
<button
type='button'
onClick={toggleLinkMenu}
onClick={() => toggleSubmenu('link')}
className={`ignore-onclickoutside ${editor.isActive('link') ? 'is-active' : ''}`}
>
<LinkSimpleHorizontal size={24} />
</button>
{ (video || image) && <span className='divider'></span> }
{ video &&
(<>
<button
type='button'
onClick={toggleVideoMenu}
onClick={() => toggleSubmenu('video')}
>
<VideoCamera size={24} />
</button>
</>)
}
{ image &&
(<>
<button
type='button'
onClick={() => toggleSubmenu('image')}
>
<Image size={24} />
</button>
</>)
}
</div>
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
{ submenu === 'link' &&
(<>
@ -227,13 +247,23 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video
<option value="dailymotion">Dailymotion</option>
</select>
<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()}>
<CheckCircle size={24} />
</button>
</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>
</>
);

View File

@ -157,6 +157,7 @@
height: 0;
width: 100%;
max-width: 600px;
margin: 1rem 0;
padding-bottom: calc(100% / 16 * 9);
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 {
position: absolute;
top: 4.5rem;

View File

@ -52,6 +52,7 @@
"@stripe/stripe-js": "^1.13.2",
"@tiptap/core": "^2.0.0-beta.174",
"@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-placeholder": "^2.0.0-beta.47",
"@tiptap/extension-underline": "^2.0.0-beta.22",

View File

@ -1560,6 +1560,11 @@
dependencies:
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":
version "2.0.0-beta.26"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.26.tgz#b00c9e32b81b1bd94eaed24bb2a22e44d5dc54a3"