From 506e5237176ff167afb0ae40f5951a396924a6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Hassan?= Date: Mon, 22 Jul 2024 16:43:54 +0200 Subject: [PATCH] feature: accordion plugin --- .../components/controls/accordion-control.tsx | 2 +- .../sn-editor-react/src/tinymce/Editor.tsx | 146 +++++++++- .../src/tinymce/controls/accordion-plugin.tsx | 251 ++++++++++++++++++ .../src/tinymce/plugins/AccordionPlugin.tsx | 24 ++ .../src/tinymce/plugins/RegisterPlugins.tsx | 12 + .../src/tinymce/plugins/index.ts | 2 + 6 files changed, 427 insertions(+), 10 deletions(-) create mode 100644 packages/sn-editor-react/src/tinymce/controls/accordion-plugin.tsx create mode 100644 packages/sn-editor-react/src/tinymce/plugins/AccordionPlugin.tsx create mode 100644 packages/sn-editor-react/src/tinymce/plugins/RegisterPlugins.tsx create mode 100644 packages/sn-editor-react/src/tinymce/plugins/index.ts diff --git a/packages/sn-editor-react/src/components/controls/accordion-control.tsx b/packages/sn-editor-react/src/components/controls/accordion-control.tsx index 319de7a1c..c40239615 100644 --- a/packages/sn-editor-react/src/components/controls/accordion-control.tsx +++ b/packages/sn-editor-react/src/components/controls/accordion-control.tsx @@ -186,7 +186,7 @@ export const AccordionControl: FC = ({ buttonProps, edito className={classes.dialog} onClose={handleSubmit} aria-labelledby="form-dialog-title" - maxWidth="md" + maxWidth="sm" fullWidth> {localization.accordionControl.title} diff --git a/packages/sn-editor-react/src/tinymce/Editor.tsx b/packages/sn-editor-react/src/tinymce/Editor.tsx index ae6106150..b36a2e6bb 100644 --- a/packages/sn-editor-react/src/tinymce/Editor.tsx +++ b/packages/sn-editor-react/src/tinymce/Editor.tsx @@ -1,6 +1,62 @@ import { Editor, IAllProps } from '@tinymce/tinymce-react' import React, { FC, useRef } from 'react' -import { Editor as TinyMCEEditor } from 'tinymce' +import { type Editor as TinyMCEEditor } from 'tinymce' +// TinyMCE so the global var exists +import 'tinymce/tinymce' +// DOM model +import 'tinymce/models/dom/model' +// import { HTMLEditorControl } from '../components/controls' +// import { RegisterPlugins } from './plugins/RegisterPlugins' +// Theme +import 'tinymce/themes/silver' +// Toolbar icons +import 'tinymce/icons/default' +// Editor styles +import 'tinymce/skins/ui/oxide/skin' + +// importing the plugin js. +// if you use a plugin that is not listed here the editor will fail to load +import 'tinymce/plugins/advlist' +import 'tinymce/plugins/anchor' +import 'tinymce/plugins/autolink' +import 'tinymce/plugins/autoresize' +import 'tinymce/plugins/autosave' +import 'tinymce/plugins/charmap' +import 'tinymce/plugins/code' +import 'tinymce/plugins/codesample' +import 'tinymce/plugins/directionality' +import 'tinymce/plugins/emoticons' +import 'tinymce/plugins/fullscreen' +import 'tinymce/plugins/help' +import 'tinymce/plugins/help/js/i18n/keynav/en' +import 'tinymce/plugins/image' +import 'tinymce/plugins/importcss' +import 'tinymce/plugins/insertdatetime' +import 'tinymce/plugins/link' +import 'tinymce/plugins/lists' +import 'tinymce/plugins/media' +import 'tinymce/plugins/nonbreaking' +import 'tinymce/plugins/pagebreak' +import 'tinymce/plugins/preview' +import 'tinymce/plugins/quickbars' +import 'tinymce/plugins/save' +import 'tinymce/plugins/searchreplace' +import 'tinymce/plugins/table' +import 'tinymce/plugins/visualblocks' +import 'tinymce/plugins/visualchars' +import 'tinymce/plugins/wordcount' +// importing plugin resources +import 'tinymce/plugins/emoticons/js/emojis' +// Content styles, including inline UI like fake cursors +import 'tinymce/skins/content/default/content' +import 'tinymce/skins/content/default/content.min.css' +import 'tinymce/skins/ui/oxide/content' +import 'tinymce/skins/ui/oxide-dark/content' +import 'tinymce/skins/ui/oxide-dark/content.min.css' +import 'tinymce/skins/ui/oxide/content.min.css' +import 'tinymce/skins/ui/oxide-dark/skin.min.css' +import 'tinymce/skins/ui/oxide/skin.min.css' +import { RegisterPlugins } from './plugins' export interface TinymceEditorProps { onChange?: IAllProps['onEditorChange'] @@ -9,16 +65,56 @@ export interface TinymceEditorProps { export const TinymceEditor: FC = (props) => { const editorRef = useRef(null) + return ( <> (editorRef.current = editor)} + onInit={(_evt, editor) => { + editorRef.current = editor + }} initialValue={props.initvalue} init={{ + skin: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'oxide-dark' : 'oxide-dark', + content_css: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'dark', + file_picker_types: 'image', + /* and here's our custom image picker*/ + file_picker_callback: (cb, _value, _meta) => { + const input = document.createElement('input') + input.setAttribute('type', 'file') + input.setAttribute('accept', 'image/*') + + input.addEventListener('change', (e) => { + let file: any + if (e.target !== null) { + file = (e.target as any).files[0] + } + + const reader = new FileReader() + reader.addEventListener('load', () => { + /* + Note: Now we need to register the blob in TinyMCEs image blob + registry. In the next release this part hopefully won't be + necessary, as we are looking to handle it internally. + */ + const id = `blobid${new Date().getTime()}` + const { blobCache } = editorRef.current?.editorUpload || {} + const base64 = (reader.result as any).split(',')[1] + const blobInfo = blobCache?.create(id, file, base64) + blobCache?.add(blobInfo!) + + /* call the callback and populate the Title field with the file name */ + cb(blobInfo?.blobUri() || '', { title: file.name }) + }) + reader.readAsDataURL(file) + }) + + input.click() + }, height: 500, - menubar: false, + menubar: true, + automatic_uploads: true, + image_title: true, plugins: [ 'advlist', 'autolink', @@ -38,12 +134,44 @@ export const TinymceEditor: FC = (props) => { 'help', 'wordcount', ], + setup: (editor) => { + RegisterPlugins({ editor }) + }, + menu: { + file: { + title: 'File', + items: + 'newdocument restoredraft | preview | importword exportpdf exportword | print | deleteallconversations', + }, + edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' }, + view: { + title: 'View', + items: + 'code revisionhistory | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments', + }, + insert: { + title: 'Insert', + items: + 'InsertAccordion link media addcomment pageembed codesample inserttable | math | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents | insertdatetime', + }, + format: { + title: 'Format', + items: + 'bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat', + }, + tools: { title: 'Tools', items: 'spellchecker spellcheckerlanguage | a11ycheck code wordcount' }, + table: { title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable' }, + help: { title: 'Help', items: 'help' }, + custom: { + title: 'test', + items: 'help', + }, + }, toolbar: - 'undo redo | blocks | ' + - 'bold italic forecolor | alignleft aligncenter ' + - 'alignright alignjustify | bullist numlist outdent indent | ' + - 'removeformat | help', - content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }', + 'fontselect | fontsizeselect | forecolor | backcolor | bold | italic | underline | alignleft | aligncenter | alignright | alignjustify | bullist | numlist | outdent | indent | link | image | print | media | code', + tools: 'inserttable', + content_style: + 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px } .tox .tox-promotion{ display:none!important}', }} onEditorChange={(e, editor) => { if (props.onChange) { diff --git a/packages/sn-editor-react/src/tinymce/controls/accordion-plugin.tsx b/packages/sn-editor-react/src/tinymce/controls/accordion-plugin.tsx new file mode 100644 index 000000000..cca177282 --- /dev/null +++ b/packages/sn-editor-react/src/tinymce/controls/accordion-plugin.tsx @@ -0,0 +1,251 @@ +import { + Button, + createStyles, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + IconButtonProps, + makeStyles, + TextField, + Theme, + Tooltip, + useTheme, +} from '@material-ui/core' +import { Close } from '@material-ui/icons' + +import React, { FC, useCallback, useRef, useState } from 'react' +import { renderToString } from 'react-dom/server' +import { Editor } from 'tinymce' +import { useLocalization } from '../../hooks' + +const useStyles = makeStyles((theme: Theme) => { + return createStyles({ + ListIcon: { + paddingLeft: '0px', + height: '32px', + '& .MuiIconButton-label': { + flexDirection: 'column', + height: 'inherit', + justifyContent: 'center', + '& .icon-container': { + '&:first-of-type': { + marginBottom: '-14px', + }, + height: '23px', + position: 'relative', + '& .down-arrow': { + top: '0', + right: '0', + position: 'absolute', + marginTop: '0px', + marginRight: '-13px', + }, + }, + }, + }, + accordion: { + display: 'flex', + flexDirection: 'column', + rowGap: '15px', + border: '2px solid', + borderColor: theme.palette.primary.main, + position: 'relative', + padding: '10px', + borderRadius: '10px', + '& .panel-close-button': { + position: 'absolute', + right: '0', + top: '0', + }, + }, + accordionContainer: { + display: 'flex', + flexDirection: 'column', + rowGap: '20px', + }, + actionPanel: { + display: 'flex', + flex: 1, + justifyContent: 'space-between', + }, + dialog: { + '& .MuiDialogContent-root': { + padding: '8px 15px', + }, + }, + }) +}) + +type TAccordions = { + title: string + body: string +} + +type PanelPros = { + title: string + body: string +} + +const Panel = ({ title, body }: PanelPros) => { + return ( +
+ +
+
{body}
+
+
+ ) +} + +const initialAccordion = { title: '', body: '' } + +interface AccordionPluginControlProps { + editor: Editor + buttonProps?: Partial + closeDialog: () => void +} + +export const AccordionPluginControl: FC = ({ editor, buttonProps, closeDialog }) => { + const [accordions, setAccordions] = useState([initialAccordion]) + const form = useRef(null) + + const theme = useTheme() + + const classes = useStyles(theme) + const localization = useLocalization() + + const handleClose = useCallback(() => { + setAccordions([initialAccordion]) + + closeDialog() + }, [closeDialog]) + + const handleSubmit = () => { + if (accordions.length === 0) { + handleClose() + return + } + + if (!form.current?.reportValidity()) { + return + } + + const panelGroup = renderToString( + <> +
+ {accordions.map((item, index) => { + return + })} +
+ {/* eslint-disable-next-line react/self-closing-comp*/} +

+ , + ) + + editor.execCommand('mceInsertContent', false, panelGroup) + + handleClose() + } + + const handleClosePanel = (index: number) => { + setAccordions((prev) => { + return prev.filter((_, i) => i !== index) + }) + } + return ( + <> + + {localization.accordionControl.title} + +
+ {accordions.map((accordion, index) => { + return ( +
+ + handleClosePanel(index)} {...buttonProps}> + + + + + { + const newAccordions = [...accordions] + + newAccordions[index] = { ...accordions[index], title: e.target.value } + + setAccordions(newAccordions) + }} + /> + + { + const newAccordions = [...accordions] + + newAccordions[index] = { ...accordions[index], body: e.target.value } + + setAccordions(newAccordions) + }} + /> +
+ ) + })} +
+
+ +
+ + +
+ + +
+
+
+
+ + ) +} diff --git a/packages/sn-editor-react/src/tinymce/plugins/AccordionPlugin.tsx b/packages/sn-editor-react/src/tinymce/plugins/AccordionPlugin.tsx new file mode 100644 index 000000000..6e53dcc21 --- /dev/null +++ b/packages/sn-editor-react/src/tinymce/plugins/AccordionPlugin.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { render, unmountComponentAtNode } from 'react-dom' +import { AccordionPluginControl } from '../controls/accordion-plugin' +import { PluginRegistrationProps } from '.' + +export const AccordionPlugin = ({ editor }: PluginRegistrationProps) => { + editor.ui.registry.addMenuItem('InsertAccordion' /*name of the plugin*/, { + text: 'Insert Accordion', + icon: 'accordion', + onAction() { + const dialogContainer = document.createElement('div') + document.body.appendChild(dialogContainer) + + const closeDialog = () => { + unmountComponentAtNode(dialogContainer) + dialogContainer.remove() + } + + const dialog = + + render(dialog, dialogContainer) + }, + }) +} diff --git a/packages/sn-editor-react/src/tinymce/plugins/RegisterPlugins.tsx b/packages/sn-editor-react/src/tinymce/plugins/RegisterPlugins.tsx new file mode 100644 index 000000000..6b84f74d3 --- /dev/null +++ b/packages/sn-editor-react/src/tinymce/plugins/RegisterPlugins.tsx @@ -0,0 +1,12 @@ +import { Editor } from 'tinymce' +import { AccordionPlugin } from './' + +export type PluginRegistrationProps = { + editor: Editor +} + +export const RegisterPlugins = ({ ...props }: PluginRegistrationProps) => { + AccordionPlugin({ ...props }) /*this is my custom component*/ +} + +export default RegisterPlugins diff --git a/packages/sn-editor-react/src/tinymce/plugins/index.ts b/packages/sn-editor-react/src/tinymce/plugins/index.ts new file mode 100644 index 000000000..e1b9aa275 --- /dev/null +++ b/packages/sn-editor-react/src/tinymce/plugins/index.ts @@ -0,0 +1,2 @@ +export * from './AccordionPlugin' +export * from './RegisterPlugins'