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:
parent
7ebdc1b06e
commit
cd5c253b3e
@ -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']));
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user