UNPKG

strapi-tiptap-editor

Version:
302 lines (277 loc) 9.65 kB
import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { Stack } from '@strapi/design-system/Stack'; import { Box } from '@strapi/design-system/Box'; import { Field, FieldLabel, FieldHint, FieldError, FieldInput, FieldAction } from '@strapi/design-system/Field'; import { Typography } from '@strapi/design-system/Typography'; import MediaLib from '../MediaLib/index.js'; import Editor from '../Editor'; import { useIntl } from 'react-intl'; import {getSettings} from "../../../../utils/api"; import defaultSettings from "../../../../utils/defaults"; import { useQuery } from 'react-query'; import Earth from "@strapi/icons/Earth" // Editor import {useEditor} from '@tiptap/react' import {Extension, mergeAttributes, wrappingInputRule} from '@tiptap/core' import UnderlineExtension from '@tiptap/extension-underline' import LinkExtension from '@tiptap/extension-link' import ImageExtension from '@tiptap/extension-image' import TextAlignExtension from '@tiptap/extension-text-align' import TableExtension from '@tiptap/extension-table' import TableRowExtension from '@tiptap/extension-table-row' import TableCellExtension from '@tiptap/extension-table-cell' import TableHeaderExtension from '@tiptap/extension-table-header' import TextStyleExtension from '@tiptap/extension-text-style' import CharacterCountExtension from '@tiptap/extension-character-count' import YouTubeExtension from '@tiptap/extension-youtube' import CodeExtension from '@tiptap/extension-code' import BoldExtension from '@tiptap/extension-bold' import ItalicExtension from '@tiptap/extension-italic' import StrikeExtension, {Strike} from '@tiptap/extension-strike' import OrderedListExtension from "@tiptap/extension-ordered-list" import BulletListExtension from '@tiptap/extension-bullet-list' import ListItemExtension from '@tiptap/extension-list-item' import GapcursorExtension from '@tiptap/extension-gapcursor' import History from '@tiptap/extension-history' import BlockquoteExtension from '@tiptap/extension-blockquote' import CodeBlockExtension from '@tiptap/extension-code-block' import DocumentExtension from '@tiptap/extension-document' import HardBreakExtension from '@tiptap/extension-hard-break' import HeadingExtension from '@tiptap/extension-heading' import HorizontalRuleExtension from '@tiptap/extension-horizontal-rule' import ParagraphExtension from '@tiptap/extension-paragraph' import TextExtension from '@tiptap/extension-text' import { Color as ColorExtension } from '@tiptap/extension-color' import HighlightExtension from '@tiptap/extension-highlight' import {mergeDeep} from "../../utils/merge"; const Wysiwyg = (opts) => { const { name, onChange, value, intlLabel, labelAction, disabled, error, description, required } = opts const {data: savedSettings, isLoading} = useQuery('settings', getSettings) const settings = mergeDeep(defaultSettings, savedSettings) if (isLoading) return null return ( <WysiwygContent name={name} onChange={onChange} value={value} intlLabel={intlLabel} labelAction={labelAction} disabled={disabled} error={error} description={description} required={required} settings={settings} /> ) } const CSSColumnsExtension = Extension.create({ name: 'cssColumns', addOptions() { return { types: [], columnTypes: [2, 3], defaultColumnType: 'two', }; }, addGlobalAttributes() { return [ { types: this.options.types, attributes: { cssColumns: { default: null, renderHTML: attributes => { if (attributes.cssColumns === null) return return { style: `column-count: ${attributes.cssColumns}`, } }, parseHTML: element => element.style.columnCount || null, }, }, } ] }, addCommands() { return { toggleColumns: (columnType) => ({commands, editor}) => { if (!editor.isActive({'cssColumns': columnType})) return this.options.types.every((type) => commands.updateAttributes(type, {cssColumns: columnType})) return this.options.types.every((type) => commands.resetAttributes(type, 'cssColumns')) }, unsetColumns: (columnType) => ({commands}) => { return this.options.types.every((type) => commands.resetAttributes(type, 'cssColumns')) }, } } }) const CustomOrderedList = OrderedListExtension.extend({ addInputRules() { return [] } }) const WysiwygContent = ({ name, onChange, value, intlLabel, labelAction, disabled, error, description, required, settings }) => { const { formatMessage } = useIntl(); const [currentContent, setCurrentContent] = useState(''); const editor = useEditor({ extensions: [ // Text DocumentExtension, ParagraphExtension, TextExtension, BoldExtension, StrikeExtension, ItalicExtension, GapcursorExtension, ListItemExtension, BulletListExtension, HeadingExtension, settings.disableOrderedListShorthand ? CustomOrderedList : OrderedListExtension, settings.code ? CodeBlockExtension : null, settings.code ? CodeExtension : null, settings.blockquote ? BlockquoteExtension : null, settings.horizontal ? HorizontalRuleExtension : null, settings.hardbreak ? HardBreakExtension : null, UnderlineExtension, TextAlignExtension.configure({ types: ['heading', 'paragraph'], }), TextStyleExtension, settings.color ? ColorExtension : null, settings.highlight ? HighlightExtension.configure({ multicolor: true }) : null, // Links settings.links.enabled ? LinkExtension.configure({ autolink: settings.links.autolink, openOnClick: settings.links.openOnClick, linkOnPaste: settings.links.linkOnPaste, HTMLAttributes: { rel: settings.links.HTMLAttributes.rel } }) : null, // Images settings.image.enabled ? ImageExtension.extend({ addAttributes() { return { ...this.parent?.(), width: { default: null }, height: { default: null }, loading: { default: null }, renderHTML: (attributes) => { return { width: attributes.width, height: attributes.height, loading: attributes.loading }; } }; } }).configure({ inline: settings.image.inline, allowBase64: settings.image.allowBase64, }) : null, // Table settings.table ? TableExtension.configure({ allowTableNodeSelection: true, }) : null, settings.table ? TableRowExtension : null, settings.table ? TableCellExtension : null, settings.table ? TableHeaderExtension : null, settings.other && settings.other.wordcount ? CharacterCountExtension.configure() : null, // CSS Columns CSSColumnsExtension.configure({ types: ['paragraph'] }), settings.youtube.enabled ? YouTubeExtension.configure({ inline: false, }) : null, History ], parseOptions: { preserveWhitespace: 'full', }, onUpdate(ctx) { if (settings.other.saveJson) { onChange({target: {name, value: JSON.stringify(ctx.editor.getJSON())}}) } else { onChange({target: {name, value: ctx.editor.getHTML()}}) } }, }) useEffect(() => { if (editor === null) return if (currentContent === '') { // Content can be 2 things: JSON or String. Be able to display both things. try { // If content is saved as json, parse it const json = JSON.parse(value) setCurrentContent(value) editor.commands.setContent(json, false) } catch (e) { // Use value as is, the content hasn't been converted to json. setCurrentContent(value) editor.commands.setContent(value, false) } } }, [editor]) return ( <Field required={required}> <Stack spacing={1}> <Box> <FieldLabel action={labelAction}> {formatMessage(intlLabel)}</FieldLabel> </Box> {editor && <Editor key="editor" disabled={disabled} name={name} editor={editor} onChange={onChange} value={value} settings={settings} /> } {error && <Typography variant="pi" textColor="danger600"> {formatMessage({ id: error, defaultMessage: error })} </Typography> } {description && <Typography variant="pi"> {formatMessage(description)} </Typography> } </Stack> </Field> ) } Wysiwyg.defaultProps = { description: '', disabled: false, error: undefined, intlLabel: '', required: false, value: '', settings: {} }; Wysiwyg.propTypes = { description: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string, }), disabled: PropTypes.bool, error: PropTypes.string, intlLabel: PropTypes.shape({ id: PropTypes.string, defaultMessage: PropTypes.string, }), labelAction: PropTypes.object, name: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, required: PropTypes.bool, value: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]), settings: PropTypes.object }; export default Wysiwyg;