From cd5c253b3e38322d25a9cc072acb679cd7bf61a4 Mon Sep 17 00:00:00 2001 From: vincent Date: Fri, 25 Mar 2022 18:44:37 +0100 Subject: [PATCH] Add video embed on the text editor --- .../base/text-editor/fab-text-editor.tsx | 13 +- .../components/base/text-editor/menu-bar.tsx | 130 +++++++++++++----- .../modules/base/fab-text-editor.scss | 60 ++++---- package.json | 6 +- yarn.lock | 6 +- 5 files changed, 143 insertions(+), 72 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 47d173953..667df3b30 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 @@ -20,15 +20,16 @@ interface FabTextEditorProps { paragraphTools?: boolean, content?: string, limit?: number, + video?: boolean, onChange?: (content: string) => void, placeholder?: string, - error?: string, + error?: string } /** * This component is a WYSIWYG text editor */ -export const FabTextEditor: React.FC = ({ label, paragraphTools, content, limit = 400, onChange, placeholder, error }) => { +export const FabTextEditor: React.FC = ({ label, paragraphTools, content, limit = 400, video, onChange, placeholder, error }) => { const { t } = useTranslation('shared'); const placeholderText = placeholder || t('app.shared.text_editor.placeholder'); // TODO: Add ctrl+click on link to visit @@ -69,7 +70,7 @@ export const FabTextEditor: React.FC = ({ label, paragraphTo <> {label && }
- +
{editor?.storage.characterCount.characters()} / {limit} @@ -85,12 +86,12 @@ export const FabTextEditor: React.FC = ({ label, paragraphTo ); }; -const FabTextEditorWrapper: React.FC = ({ label, paragraphTools, content, limit, placeholder, error }) => { +const FabTextEditorWrapper: React.FC = ({ label, paragraphTools, content, limit, video, placeholder, error }) => { return ( - + ); }; -Application.Components.component('fabTextEditor', react2angular(FabTextEditorWrapper, ['label', 'paragraphTools', 'content', 'limit', 'placeholder', 'error'])); +Application.Components.component('fabTextEditor', react2angular(FabTextEditorWrapper, ['label', 'paragraphTools', 'content', 'limit', 'video', '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 0448aa3c6..6809feca2 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 @@ -6,34 +6,39 @@ import { TextAa, TextBolder, TextItalic, TextUnderline, LinkSimpleHorizontal, Li interface MenuBarProps { paragraphTools?: boolean, - extra?: boolean, + video?: boolean, editor?: Editor, } /** * This component is the menu bar for the WYSIWYG text editor */ -export const MenuBar: React.FC = ({ editor, paragraphTools, extra }) => { +export const MenuBar: React.FC = ({ editor, paragraphTools, video }) => { const { t } = useTranslation('shared'); - const [linkMenu, setLinkMenu] = useState(false); + const [submenu, setSubmenu] = useState(''); const resetUrl = { href: '', target: '_blank' }; const [url, setUrl] = useState(resetUrl); - const ref = useOnclickOutside(() => { - setLinkMenu(false); - }); + const [videoProvider, setVideoProvider] = useState('youtube'); + const [videoId, setVideoId] = useState(''); - // Reset state values when the link menu is closed + // Reset state values when the submenu is closed useEffect(() => { - if (!linkMenu) { + if (!submenu) { setUrl(resetUrl); + setVideoProvider('youtube'); } - }, [linkMenu]); + }, [submenu]); + + // Close the submenu frame on click outside + const ref = useOnclickOutside(() => { + setSubmenu(''); + }); // Toggle link menu's visibility const toggleLinkMenu = () => { - if (!linkMenu) { - setLinkMenu(true); + if (submenu !== 'link') { + setSubmenu('link'); const previousUrl = { href: editor.getAttributes('link').href, target: editor.getAttributes('link').target || '' @@ -43,8 +48,7 @@ export const MenuBar: React.FC = ({ editor, paragraphTools, extra setUrl(previousUrl); } } else { - setLinkMenu(false); - setUrl(resetUrl); + setSubmenu(''); } }; @@ -56,7 +60,7 @@ export const MenuBar: React.FC = ({ editor, paragraphTools, extra }; // Update url - const handleChange = (evt) => { + const linkUrlChange = (evt) => { setUrl({ ...url, href: evt.target.value }); }; // Support keyboard "Enter" key event to validate @@ -74,19 +78,52 @@ export const MenuBar: React.FC = ({ editor, paragraphTools, extra } editor.chain().focus().extendMarkRange('link').setLink({ href: url.href, target: url.target }).run(); if (closeLinkMenu) { - setLinkMenu(false); + setSubmenu(''); } }, [editor, url]); // Remove the link tag from the selected text const unsetLink = () => { editor.chain().focus().extendMarkRange('link').unsetLink().run(); - setLinkMenu(false); + setSubmenu(''); }; - // Add iFrame + // 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 id = evt.target.value.match(/([^/]+$)/g); + setVideoId(id); + }; + // Insert iframe containing the video player const addIframe = () => { - editor.chain().focus().setIframe({ src: 'https://www.youtube.com/embed/XIMLoLxmTDw' }).run(); + let videoUrl = ''; + switch (videoProvider) { + case 'youtube': + videoUrl = `https://www.youtube.com/embed/${videoId}`; + break; + case 'vimeo': + videoUrl = `https://player.vimeo.com/video/${videoId}`; + break; + case 'dailymotion': + videoUrl = `https://www.dailymotion.com/embed/video/${videoId}`; + break; + default: + break; + } + editor.chain().focus().setIframe({ src: videoUrl }).run(); + setSubmenu(''); }; if (!editor) { @@ -150,34 +187,53 @@ export const MenuBar: React.FC = ({ editor, paragraphTools, extra > - { extra && + { video && (<> ) }
-
-
- - -
-
- - -
+
+ { submenu === 'link' && + (<> +
+ + +
+
+ + +
+ ) + } + { submenu === 'video' && + (<> + +
+ + +
+ ) + }
); 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 5ec63f5a2..8da40fedd 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-text-editor.scss @@ -49,7 +49,10 @@ // tiptap class for the editor .ProseMirror { + max-height: 40vh; padding: 1.6rem 1.6rem 1.2rem; + overflow: auto; + resize: vertical; &:focus { outline: none; } @include editor; } @@ -61,7 +64,7 @@ color: var(--gray-hard-lightest); } - &-linkMenu { + &-subMenu { position: absolute; top: 4.5rem; right: 0; @@ -83,25 +86,35 @@ } & > div { display: flex; - align-items: center + align-items: center; + &:not(:last-of-type) { margin-bottom: 0.8rem; } } - .url { + + input[type="text"], + select { + width: 100%; + height: 4rem; + padding: 0.4rem 0.8rem; + background-color: var(--gray-soft-light); + border: 1px solid var(--secondary); + border-radius: var(--border-radius); + font-size: var(--text-base); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + input[type="text"] { + margin-right: 1.2rem; + &::placeholder { color: var(--gray-soft-darkest);} + } + select { margin-bottom: 0.8rem; - input { - width: 100%; - height: 4rem; - margin-right: 1.2rem; - padding: 0.4rem 0.8rem; - background-color: var(--gray-soft-light); - border: 1px solid var(--secondary); - border-radius: var(--border-radius); - font-size: var(--text-base); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - &::placeholder { color: var(--gray-soft-darkest);} - } } + button { + @include button(3.2rem); + margin-left: auto; + } + .tab { display: flex; align-items: center; @@ -137,11 +150,6 @@ border-color: var(--information); } } - - button { - @include button(3.2rem); - margin-left: auto; - } } &-video { @@ -149,8 +157,14 @@ height: 0; width: 100%; max-width: 600px; - padding-bottom: calc(100 / 16 * 9); + padding-bottom: calc(100% / 16 * 9); overflow: hidden; + + iframe { + position: absolute; + max-width: 100%; + inset: 0; + } } &-error { diff --git a/package.json b/package.json index 16c1ae9b9..9dd389c73 100644 --- a/package.json +++ b/package.json @@ -51,13 +51,13 @@ "@lyracom/embedded-form-glue": "^0.3.3", "@stripe/react-stripe-js": "^1.4.0", "@stripe/stripe-js": "^1.13.2", - "@tiptap/core": "^2.0.0-beta.1", + "@tiptap/core": "^2.0.0-beta.174", "@tiptap/extension-character-count": "^2.0.0-beta.24", "@tiptap/extension-link": "^2.0.0-beta.36", "@tiptap/extension-placeholder": "^2.0.0-beta.47", "@tiptap/extension-underline": "^2.0.0-beta.22", - "@tiptap/react": "^2.0.0-beta.107", - "@tiptap/starter-kit": "^2.0.0-beta.180", + "@tiptap/react": "^2.0.0-beta.108", + "@tiptap/starter-kit": "^2.0.0-beta.183", "@types/angular": "^1.7.3", "@types/prop-types": "^15.7.2", "@types/react": "^17.0.3", diff --git a/yarn.lock b/yarn.lock index 2e023a086..822c6d7e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1415,7 +1415,7 @@ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.15.1.tgz#a3809ecc5aa8a03bd261a2f970d11cfdcbf11c4f" integrity sha512-yJiDGutlwu25iajCy51VRJeoH3UMs+s5qVIDGfmPUuFpZ+F6AJ9g9EFrsBNvHxAGBahQFMLlBdzlCVydhGp6tg== -"@tiptap/core@^2.0.0-beta.1", "@tiptap/core@^2.0.0-beta.175": +"@tiptap/core@^2.0.0-beta.174", "@tiptap/core@^2.0.0-beta.175": version "2.0.0-beta.175" resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.0.0-beta.175.tgz#d23f6e60308cde192451121e21df83c256bdcd92" integrity sha512-dDf+2GtifskNLysn49kaCIz0o5hf6VDZ8J7jSQAfoPDEkEkfw9OKhWrR7NzWW6J34CSJreFDRiWkGt8Qz283Vg== @@ -1587,7 +1587,7 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-underline/-/extension-underline-2.0.0-beta.23.tgz#050a31ac55b7ad63e8abf57ac941c62e255f57b8" integrity sha512-pMjFH/NpFWLd2XQQa5rG9rGVQ9mu3ygdtu6VGfJ3aAjzBiyLXDKhE4biIFWyFsr8zLpp7DjwbrmLV0UGvbG1WQ== -"@tiptap/react@^2.0.0-beta.107": +"@tiptap/react@^2.0.0-beta.108": version "2.0.0-beta.109" resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.0.0-beta.109.tgz#0998989f7f81e2f90a10fb37c55d531d7015bbee" integrity sha512-kx/I+9DbiKX+LPFYTQf1Mycbw4U77nRsuztMi5UyGoONnwVwVxOUN6sxdnsNX0uo/H0Rf5ZAtQn8vQBaTWPzsQ== @@ -1596,7 +1596,7 @@ "@tiptap/extension-floating-menu" "^2.0.0-beta.51" prosemirror-view "^1.23.6" -"@tiptap/starter-kit@^2.0.0-beta.180": +"@tiptap/starter-kit@^2.0.0-beta.183": version "2.0.0-beta.184" resolved "https://registry.yarnpkg.com/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.184.tgz#c621d51d9cb8a1b18cbec7af6080ce8288bdce72" integrity sha512-FgF94i5RQzXiGAIkaubnXEaYwJfiZRbMPZcmarwNo8IyqPnLT34Q1yjw/qZ3nv7rDehWV5l/zenbrrNtPYVCkA==