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

Add video embed on the text editor

This commit is contained in:
vincent 2022-03-25 18:44:37 +01:00 committed by Sylvain
parent 7ebdc1b06e
commit cd5c253b3e
5 changed files with 143 additions and 72 deletions

View File

@ -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<FabTextEditorProps> = ({ label, paragraphTools, content, limit = 400, onChange, placeholder, error }) => {
export const FabTextEditor: React.FC<FabTextEditorProps> = ({ 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<FabTextEditorProps> = ({ label, paragraphTo
<>
{label && <label onClick={focusEditor} className="fab-textEditor-label">{label}</label>}
<div className="fab-textEditor">
<MenuBar editor={editor} paragraphTools={paragraphTools} />
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} />
<EditorContent editor={editor} />
<div className="fab-textEditor-character-count">
{editor?.storage.characterCount.characters()} / {limit}
@ -85,12 +86,12 @@ export const FabTextEditor: React.FC<FabTextEditorProps> = ({ label, paragraphTo
);
};
const FabTextEditorWrapper: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit, placeholder, error }) => {
const FabTextEditorWrapper: React.FC<FabTextEditorProps> = ({ label, paragraphTools, content, limit, video, placeholder, error }) => {
return (
<Loader>
<FabTextEditor label={label} paragraphTools={paragraphTools} content={content} limit={limit} placeholder={placeholder} error={error} />
<FabTextEditor label={label} paragraphTools={paragraphTools} content={content} limit={limit} video={video} placeholder={placeholder} error={error} />
</Loader>
);
};
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']));

View File

@ -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<MenuBarProps> = ({ editor, paragraphTools, extra }) => {
export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video }) => {
const { t } = useTranslation('shared');
const [linkMenu, setLinkMenu] = useState<boolean>(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<MenuBarProps> = ({ editor, paragraphTools, extra
setUrl(previousUrl);
}
} else {
setLinkMenu(false);
setUrl(resetUrl);
setSubmenu('');
}
};
@ -56,7 +60,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ 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<MenuBarProps> = ({ 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,20 +187,22 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, extra
>
<LinkSimpleHorizontal size={24} />
</button>
{ extra &&
{ video &&
(<>
<button
type='button'
onClick={() => addIframe()}
onClick={toggleVideoMenu}
>
<VideoCamera size={24} />
</button>
</>)
}
</div>
<div ref={ref} className={`fab-textEditor-linkMenu ${linkMenu ? 'is-active' : ''}`}>
<div className="url">
<input value={url.href} onChange={handleChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.link_placeholder')} />
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
{ submenu === 'link' &&
(<>
<div>
<input value={url.href} onChange={linkUrlChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.link_placeholder')} />
<button type='button' onClick={unsetLink}>
<Trash size={24} />
</button>
@ -178,6 +217,23 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, extra
<CheckCircle size={24} />
</button>
</div>
</>)
}
{ submenu === 'video' &&
(<>
<select name="provider" onChange={handleSelect}>
<option value="youtube">YouTube</option>
<option value="vimeo">Vimeo</option>
<option value="dailymotion">Dailymotion</option>
</select>
<div>
<input type="text" onChange={VideoUrlChange} placeholder={t('app.shared.text_editor.link_placeholder')} />
<button type='button' onClick={() => addIframe()}>
<CheckCircle size={24} />
</button>
</div>
</>)
}
</div>
</>
);

View File

@ -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,14 +86,14 @@
}
& > div {
display: flex;
align-items: center
align-items: center;
&:not(:last-of-type) { margin-bottom: 0.8rem; }
}
.url {
margin-bottom: 0.8rem;
input {
input[type="text"],
select {
width: 100%;
height: 4rem;
margin-right: 1.2rem;
padding: 0.4rem 0.8rem;
background-color: var(--gray-soft-light);
border: 1px solid var(--secondary);
@ -99,9 +102,19 @@
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;
}
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 {

View File

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

View File

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