Skip to content

Commit

Permalink
Merge pull request #1621 from SenseNet/feature/Accordion-in-Tiptap-ed…
Browse files Browse the repository at this point in the history
…itor

Accordion in Rich Text Editor
  • Loading branch information
hassanad94 authored Jun 10, 2024
2 parents d840117 + e2e92df commit 40eba8f
Show file tree
Hide file tree
Showing 19 changed files with 833 additions and 9 deletions.
269 changes: 269 additions & 0 deletions packages/sn-editor-react/src/components/controls/accordion-control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import {
Button,
createStyles,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
IconButtonProps,
makeStyles,
TextField,
Theme,
Tooltip,
useTheme,
} from '@material-ui/core'
import { ArrowDropDown, Close, DragHandle } from '@material-ui/icons'

import { Editor } from '@tiptap/react'
import React, { FC, useCallback, useRef, useState } from 'react'
import { renderToString } from 'react-dom/server'
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',
},
},
})
})

interface AccordionControlProps {
editor: Editor
buttonProps?: Partial<IconButtonProps>
}

type TAccordions = {
title: string
body: string
}

type PanelPros = {
title: string
body: string
}

const Panel = ({ title, body }: PanelPros) => {
return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">
<a href="#collapse1716279617835-0" className="collapsed">
<i className="svg" />
{title}
</a>
</h3>
</div>
<div className="panel-collapse collapse">
<div className="panel-body">{body}</div>
</div>
</div>
)
}

const initialAccordion = { title: '', body: '' }

export const AccordionControl: FC<AccordionControlProps> = ({ buttonProps, editor }) => {
const [open, setOpen] = useState(false)

const [accordions, setAccordions] = useState<TAccordions[]>([initialAccordion])
const form = useRef<HTMLFormElement>(null)

const theme = useTheme()

const classes = useStyles(theme)
const localization = useLocalization()

const handleClickOpen = () => {
setOpen(true)
}

const handleClose = useCallback(() => {
setAccordions([initialAccordion])

setOpen(false)
}, [])

const handleSubmit = () => {
if (accordions.length === 0) {
handleClose()
return
}

if (!form.current?.reportValidity()) {
return
}

const panelGroup = renderToString(
<>
<div className="panel-group">
{accordions.map((item, index) => {
return <Panel key={index} title={item.title} body={item.body} />
})}
</div>
{/* eslint-disable-next-line react/self-closing-comp*/}
<p></p>
</>,
)

editor.chain().focus().insertContent(panelGroup).run()

handleClose()
}

const handleClosePanel = (index: number) => {
setAccordions((prev) => {
return prev.filter((_, i) => i !== index)
})
}

return (
<>
<Tooltip title={localization.accordionControl.title}>
<IconButton className={classes.ListIcon} onClick={handleClickOpen} {...buttonProps}>
<div className="icon-container">
<ArrowDropDown className="down-arrow" />
<DragHandle />
</div>
<div className="icon-container">
<ArrowDropDown className="down-arrow" />
<DragHandle />
</div>
</IconButton>
</Tooltip>
<Dialog
open={open}
className={classes.dialog}
onClose={handleSubmit}
aria-labelledby="form-dialog-title"
maxWidth="md"
fullWidth>
<DialogTitle id="form-dialog-title">{localization.accordionControl.title}</DialogTitle>
<DialogContent>
<form ref={form} className={classes.accordionContainer}>
{accordions.map((accordion, index) => {
return (
<div key={index} className={classes.accordion}>
<Tooltip title={localization.accordionControl.closePanel}>
<IconButton className="panel-close-button" onClick={() => handleClosePanel(index)} {...buttonProps}>
<Close color="error" />
</IconButton>
</Tooltip>

<TextField
autoFocus
margin="dense"
label={localization.accordionControl.title}
type="text"
required
fullWidth
value={accordion.title}
onChange={(e) => {
const newAccordions = [...accordions]

newAccordions[index] = { ...accordions[index], title: e.target.value }

setAccordions(newAccordions)
}}
/>

<TextField
multiline
autoFocus
margin="dense"
label={localization.accordionControl.body}
type="text"
required
fullWidth
value={accordion.body}
onChange={(e) => {
const newAccordions = [...accordions]

newAccordions[index] = { ...accordions[index], body: e.target.value }

setAccordions(newAccordions)
}}
/>
</div>
)
})}
</form>
</DialogContent>
<DialogActions>
<div className={classes.actionPanel}>
<Button
onClick={() =>
setAccordions((prev) => {
return [...prev, initialAccordion]
})
}
title={localization.accordionControl.addPanel}>
{localization.accordionControl.addPanel}
</Button>

<div className="close-actions">
<Button onClick={handleClose}>{localization.common.cancel}</Button>
<Button
onClick={() => {
handleSubmit()
}}
color="primary">
{localization.imageControl.submit}
</Button>
</div>
</div>
</DialogActions>
</Dialog>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const HTMLEditorControl: FC<HTMLEditorControlProps> = ({ editor, buttonPr
setOpen(true)
}

const saveHTMLContent = () => {
editor.chain().focus().setContent(html, true).run()
setOpen(false)
}

const handleClose = () => {
if (editor.getHTML() === html) {
setOpen(false)
Expand All @@ -38,9 +43,7 @@ export const HTMLEditorControl: FC<HTMLEditorControlProps> = ({ editor, buttonPr
if (!confirmResult) {
return
}
editor.commands.setContent(html)

setOpen(false)
saveHTMLContent()
}

return (
Expand All @@ -60,7 +63,7 @@ export const HTMLEditorControl: FC<HTMLEditorControlProps> = ({ editor, buttonPr
aria-labelledby="html-editor-control-title"
fullWidth
maxWidth="lg"
onExited={() => { }}>
onExited={() => {}}>
<DialogTitle id="html-editor-control-title">{localization.HTMLEditorControl.title}</DialogTitle>
<DialogContent>
<HtmlEditor initialState={editor.getHTML()} fieldOnChange={setHtml} />
Expand All @@ -74,8 +77,7 @@ export const HTMLEditorControl: FC<HTMLEditorControlProps> = ({ editor, buttonPr
</Button>
<Button
onClick={() => {
editor.commands.setContent(html)
setOpen(false)
saveHTMLContent()
}}
color="primary">
{localization.linkControl.submit}
Expand Down
1 change: 1 addition & 0 deletions packages/sn-editor-react/src/components/controls/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './link-control'
export * from './table-control'
export * from './typography-control'
export * from './html-editor-control'
export * from './accordion-control'
48 changes: 48 additions & 0 deletions packages/sn-editor-react/src/components/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,54 @@ const useStyles = makeStyles((theme) => {
borderRadius: commonStyles.editorBorderRadius,
border: theme.palette.type === 'dark' ? commonStyles.editorBorder : 'unset',

'& .panel-group': {
paddingBlock: '10px',
display: 'flex',
flexDirection: 'column',
alignItems: 'strech',
rowGap: '20px',
'& .panel': {
border: '2px solid',
borderColor: theme.palette.primary.main,
borderRadius: '8px',
position: 'relative',
maxWidth: '100%',
paddingBlock: '5px',
'& .panel-heading': {
borderBottom: '1px solid',
borderBottomColor: theme.palette.primary.main,
'& h3': {
margin: '0px',
padding: '0px 30px 5px 10px',
display: 'block',
'& a ': {
textDecoration: 'none',
color: 'currentColor',
letterSpacing: '1px',
fontWeight: '500',
},
},
},
'& .panel-body': {
paddingInline: '10px',
paddingTop: '10px',
'& .panel-collapse': {
paddingRight: '10px',
},
},
'&:after': {
content: '"➧"',
position: 'absolute',
top: '0',
right: '0',
transform: 'rotate(90deg)',
marginTop: '-1px',
fontSize: '26px',
marginRight: '5px',
},
},
},

'& .ProseMirror': {
outline: 0,

Expand Down
Loading

0 comments on commit 40eba8f

Please sign in to comment.