From 67f678a2821b8ce15faf5bf6f6534b843faa1892 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 28 Mar 2022 14:01:14 +0200 Subject: [PATCH] Add image embed in the text editor --- .../base/text-editor/fab-text-editor.tsx | 19 +++-- .../components/base/text-editor/menu-bar.tsx | 84 +++++++++++++------ .../modules/base/fab-text-editor.scss | 11 +++ package.json | 1 + yarn.lock | 5 ++ 5 files changed, 87 insertions(+), 33 deletions(-) diff --git a/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx b/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx index 667df3b30..f3c45a331 100644 --- a/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx +++ b/app/frontend/src/javascript/components/base/text-editor/fab-text-editor.tsx @@ -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 = ({ label, paragraphTools, content, limit = 400, video, onChange, placeholder, error }) => { +export const FabTextEditor: React.FC = ({ 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 = ({ 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 = ({ label, paragraphTo <> {label && }
- +
{editor?.storage.characterCount.characters()} / {limit} @@ -86,12 +93,12 @@ export const FabTextEditor: React.FC = ({ label, paragraphTo ); }; -const FabTextEditorWrapper: React.FC = ({ label, paragraphTools, content, limit, video, placeholder, error }) => { +const FabTextEditorWrapper: React.FC = ({ label, paragraphTools, content, limit, video, image, placeholder, error }) => { return ( - + ); }; -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'])); diff --git a/app/frontend/src/javascript/components/base/text-editor/menu-bar.tsx b/app/frontend/src/javascript/components/base/text-editor/menu-bar.tsx index 6809feca2..b707db9c3 100644 --- a/app/frontend/src/javascript/components/base/text-editor/menu-bar.tsx +++ b/app/frontend/src/javascript/components/base/text-editor/menu-bar.tsx @@ -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 = ({ editor, paragraphTools, video }) => { +export const MenuBar: React.FC = ({ editor, paragraphTools, video, image }) => { const { t } = useTranslation('shared'); const [submenu, setSubmenu] = useState(''); @@ -21,12 +22,14 @@ export const MenuBar: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ editor, paragraphTools, video + { (video || image) && } { video && (<> ) } + { image && + (<> + + ) + }
+
{ submenu === 'link' && (<> @@ -227,13 +247,23 @@ export const MenuBar: React.FC = ({ editor, paragraphTools, video
- +
) } + { submenu === 'image' && + (<> +
+ + +
+ ) + }
); diff --git a/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss b/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss index 8da40fedd..68422a09c 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss @@ -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; diff --git a/package.json b/package.json index 9dd389c73..aa015b2f8 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,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", diff --git a/yarn.lock b/yarn.lock index 822c6d7e9..ae22cbdd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1534,6 +1534,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"