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:
parent
4c6a1fb0fc
commit
d1daa55be1
@ -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']));
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user