Source

adminjs-design-system/src/molecules/rich-text/rich-text.tsx

  1. /* eslint-disable @typescript-eslint/ban-ts-comment */
  2. /* eslint-disable import/order */
  3. /* eslint-disable @typescript-eslint/no-unused-vars */
  4. /* eslint-disable global-require */
  5. /* eslint-disable @typescript-eslint/no-var-requires */
  6. import React, { useState, useEffect, useRef, useMemo, useCallback, forwardRef } from 'react'
  7. import styled from 'styled-components'
  8. import snow from './snow.styles'
  9. import bubble from './bubble.styles'
  10. import styles from './styles'
  11. import Box from '../../atoms/box/box'
  12. import { Quill as QuillClass } from 'quill/index'
  13. import { DefaultQuillToolbarOptions, RichTextProps } from './rich-text-props'
  14. // Following hack is done for SSR case, where Quill wants to invoke `document.createElement...`
  15. // So when system sees that file is run by the server (window is not defined) then it sets
  16. // quill to null instead throwing errors. We have to use require because import has to be
  17. // top-level. This line cannot be changed since rollup bundler relies on it in the exact form.
  18. // Check out `config/rollup.js`
  19. // @ts-ignore
  20. const Quill: typeof QuillClass = typeof window === 'object' ? require('quill') : null
  21. const Theme = styled(Box)<RichTextProps>`
  22. ${bubble};
  23. ${snow};
  24. ${styles};
  25. `
  26. /**
  27. * @load ./rich-text.doc.md
  28. * @component
  29. * @subcategory Molecules
  30. * @hideconstructor
  31. * @see RichTextProps
  32. * @see {@link https://storybook.adminjs.co/?path=/story/designsystem-molecules-rich-text--default Storybook}
  33. * @new In version 3.3
  34. * @section design-system
  35. */
  36. export const RichText = forwardRef<HTMLDivElement, RichTextProps>((props, ref) => {
  37. const { value: initialValue, borderless, quill: options, onChange } = props
  38. options.theme = options.theme || 'snow'
  39. if (!options.modules?.toolbar) {
  40. options.modules = options.modules || {}
  41. options.modules.toolbar = DefaultQuillToolbarOptions
  42. }
  43. if (!Quill) {
  44. return <div>Server Side Rendered</div>
  45. }
  46. const classNames: Array<string> = []
  47. if (borderless) {
  48. classNames.push('quill-borderless')
  49. }
  50. const [quill, setQuill] = useState<QuillClass | null>(null)
  51. const [content, setContent] = useState<string>(initialValue || '')
  52. // TODO: right now I don't watch for changes on ref - maybe I should?
  53. const editorRef = ref as React.RefObject<HTMLDivElement> || useRef<HTMLDivElement>(null)
  54. const handleChange = useCallback(() => {
  55. const editor = quill?.root
  56. if (editor) {
  57. const currentContent = editor.innerHTML
  58. setContent(currentContent)
  59. if (onChange) {
  60. onChange(currentContent)
  61. }
  62. }
  63. }, [onChange, quill])
  64. useEffect(() => {
  65. if (editorRef.current) {
  66. const quillInstance = new Quill(editorRef.current, options)
  67. setQuill(quillInstance)
  68. }
  69. return () => {
  70. setQuill(null)
  71. }
  72. }, [])
  73. useEffect(() => {
  74. if (!editorRef.current || !quill) {
  75. return
  76. }
  77. if (content && quill.root.innerHTML !== content) {
  78. quill.clipboard.dangerouslyPasteHTML(content)
  79. }
  80. }, [quill]) // only when quill is initialized - later on it should update content
  81. useEffect(() => {
  82. const editor = quill?.root
  83. if (!editor) {
  84. return undefined
  85. }
  86. editor?.addEventListener('DOMSubtreeModified', handleChange)
  87. return () => {
  88. editor?.removeEventListener('DOMSubtreeModified', handleChange)
  89. }
  90. }, [onChange, handleChange])
  91. return (
  92. <Theme quill={options}>
  93. <div className={classNames.join(' ')}>
  94. <div
  95. className="quill-editor"
  96. ref={editorRef}
  97. />
  98. </div>
  99. </Theme>
  100. )
  101. })
  102. export default RichText