UNPKG

kedao

Version:

Rich Text Editor Based On Draft.js

492 lines (491 loc) 23.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { classNameParser } from '../../utils/style'; import React, { useImperativeHandle, forwardRef, useMemo, useState, useRef, useEffect, useCallback } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { selectionHasInlineStyle, getSelectionBlockType, getSelectionEntityType, toggleSelectionInlineStyle, toggleSelectionBlockType, toggleSelectionEntity, insertMedias } from '../../utils'; import loadable from '@loadable/component'; import styles from "./style.module.css"; import useLanguage from '../../hooks/use-language'; import { tablerIconProps } from '../../constants'; import SuperscriptIcon from 'tabler-icons-react/dist/icons/superscript'; import SubscriptIcon from 'tabler-icons-react/dist/icons/subscript'; import BlockquoteIcon from 'tabler-icons-react/dist/icons/blockquote'; import MusicIcon from 'tabler-icons-react/dist/icons/music'; import ArrowBackUpIcon from 'tabler-icons-react/dist/icons/arrow-back-up'; import ArrowForwardUpIcon from 'tabler-icons-react/dist/icons/arrow-forward-up'; import EraserIcon from 'tabler-icons-react/dist/icons/eraser'; import MinusIcon from 'tabler-icons-react/dist/icons/minus'; import BoldIcon from 'tabler-icons-react/dist/icons/bold'; import CodeIcon from 'tabler-icons-react/dist/icons/code'; import ItalicIcon from 'tabler-icons-react/dist/icons/italic'; import ListIcon from 'tabler-icons-react/dist/icons/list'; import ListNumbersIcon from 'tabler-icons-react/dist/icons/list-numbers'; import MaximizeIcon from 'tabler-icons-react/dist/icons/maximize'; import MaximizeOffIcon from 'tabler-icons-react/dist/icons/maximize-off'; import MoodEmptyIcon from 'tabler-icons-react/dist/icons/mood-empty'; import StrikethroughIcon from 'tabler-icons-react/dist/icons/strikethrough'; import UnderlineIcon from 'tabler-icons-react/dist/icons/underline'; import TrashIcon from 'tabler-icons-react/dist/icons/trash'; const cls = classNameParser(styles); const Finder = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../Finder'); })); const LinkEditor = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../LinkEditor'); })); const HeadingPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../Headings'); })); const TextColorPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../TextColor'); })); const FontSizePicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../FontSize'); })); const LineHeightPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../LineHeight'); })); const FontFamilyPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../FontFamily'); })); const TextAlign = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../TextAlign'); })); const EmojiPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../EmojiPicker'); })); const LetterSpacingPicker = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../LetterSpacing'); })); const TextIndent = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../TextIndent'); })); const DropDown = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../DropDown'); })); const Button = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../Button'); })); const Modal = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../Modal'); })); const HTMLButton = loadable(() => __awaiter(void 0, void 0, void 0, function* () { return yield import('../HTML'); })); const isModalControl = (control) => { return control.type === 'modal'; }; const isButtonControl = (control) => { return control.type === 'button'; }; const isDropDownControl = (control) => { return control.type === 'dropdown'; }; const exclusiveInlineStyles = { superscript: 'subscript', subscript: 'superscript' }; const getEditorControlMap = (lang, isFullscreen, mode) => { return { undo: { key: 'undo', title: lang.controls.undo, text: React.createElement(ArrowBackUpIcon, Object.assign({}, tablerIconProps)), type: 'editor-method', command: 'undo' }, redo: { key: 'redo', title: lang.controls.redo, text: React.createElement(ArrowForwardUpIcon, Object.assign({}, tablerIconProps)), type: 'editor-method', command: 'redo' }, 'remove-styles': { key: 'remove-styles', title: lang.controls.removeStyles, text: React.createElement(EraserIcon, Object.assign({}, tablerIconProps)), type: 'editor-method', command: 'removeSelectionInlineStyles' }, hr: { key: 'hr', title: lang.controls.hr, text: React.createElement(MinusIcon, Object.assign({}, tablerIconProps)), type: 'editor-method', command: 'insertHorizontalLine' }, bold: { key: 'bold', title: lang.controls.bold, text: React.createElement(BoldIcon, Object.assign({}, tablerIconProps)), type: 'inline-style', command: 'bold' }, italic: { key: 'italic', title: lang.controls.italic, text: React.createElement(ItalicIcon, Object.assign({}, tablerIconProps)), type: 'inline-style', command: 'italic' }, underline: { key: 'underline', title: lang.controls.underline, text: React.createElement(UnderlineIcon, Object.assign({}, tablerIconProps)), type: 'inline-style', command: 'underline' }, 'strike-through': { key: 'strike-through', title: lang.controls.strikeThrough, text: React.createElement(StrikethroughIcon, Object.assign({}, tablerIconProps)), type: 'inline-style', command: 'strikethrough' }, superscript: { key: 'superscript', title: lang.controls.superScript, text: React.createElement(SuperscriptIcon, { strokeWidth: 1, size: 20 }), type: 'inline-style', command: 'superscript' }, subscript: { key: 'subscript', title: lang.controls.subScript, text: React.createElement(SubscriptIcon, { strokeWidth: 1, size: 20 }), type: 'inline-style', command: 'subscript' }, headings: { key: 'headings', title: lang.controls.headings, type: 'headings' }, blockquote: { key: 'blockquote', title: lang.controls.blockQuote, text: React.createElement(BlockquoteIcon, { strokeWidth: 1, size: 20 }), type: 'block-type', command: 'blockquote' }, code: { key: 'code', title: lang.controls.code, text: React.createElement(CodeIcon, Object.assign({}, tablerIconProps)), type: 'block-type', command: 'code-block' }, 'list-ul': { key: 'list-ul', title: lang.controls.unorderedList, text: React.createElement(ListIcon, Object.assign({}, tablerIconProps)), type: 'block-type', command: 'unordered-list-item' }, 'list-ol': { key: 'list-ol', title: lang.controls.orderedList, text: React.createElement(ListNumbersIcon, Object.assign({}, tablerIconProps)), type: 'block-type', command: 'ordered-list-item' }, link: { key: 'link', title: lang.controls.link, type: 'link' }, 'text-color': { key: 'text-color', title: lang.controls.color, type: 'text-color' }, 'line-height': { key: 'line-height', title: lang.controls.lineHeight, type: 'line-height' }, 'letter-spacing': { key: 'letter-spacing', title: lang.controls.letterSpacing, type: 'letter-spacing' }, 'text-indent': { key: 'text-indent', title: lang.controls.textIndent, type: 'text-indent' }, 'font-size': { key: 'font-size', title: lang.controls.fontSize, type: 'font-size' }, 'font-family': { key: 'font-family', title: lang.controls.fontFamily, type: 'font-family' }, 'text-align': { key: 'text-align', title: lang.controls.textAlign, type: 'text-align' }, media: { key: 'media', title: lang.controls.media, text: React.createElement(MusicIcon, { strokeWidth: 1, size: 20 }), type: 'media' }, emoji: { key: 'emoji', title: lang.controls.emoji, text: React.createElement(MoodEmptyIcon, Object.assign({}, tablerIconProps)), type: 'emoji' }, clear: { key: 'clear', title: lang.controls.clear, text: React.createElement(TrashIcon, Object.assign({}, tablerIconProps)), type: 'editor-method', command: 'clearEditorContent' }, fullscreen: { key: 'fullscreen', title: isFullscreen ? lang.controls.exitFullscreen : lang.controls.fullscreen, text: isFullscreen ? (React.createElement(MaximizeOffIcon, Object.assign({}, tablerIconProps))) : (React.createElement(MaximizeIcon, Object.assign({}, tablerIconProps))), type: 'editor-method', command: 'toggleFullscreen' }, html: { key: 'html', title: 'HTML', text: React.createElement(HTMLButton, { mode: mode }), type: 'editor-method', command: 'toggleHtml' }, modal: { key: 'modal', type: 'modal' }, button: { key: 'button', type: 'button' }, dropdown: { key: 'dropdown', type: 'dropdown' }, component: { key: 'component', type: 'component' } }; }; const mergeControls = (builtInControls, parsedExtendControls) => { if (!((parsedExtendControls === null || parsedExtendControls === void 0 ? void 0 : parsedExtendControls.length) > 0)) { return builtInControls; } return builtInControls .map((item) => { return (parsedExtendControls.find((subItem) => { return subItem.replace === (item.key || item); }) || item); }) .concat(parsedExtendControls.filter((item) => { return typeof item === 'string' || !item.replace; })); }; const ControlBar = forwardRef(({ editorState, media, allowInsertLinkText, className, controls, editorId, extendControls, getContainerNode, style, textBackgroundColor, onRequestFocus, isFullscreen, mode, commands, onChange }, ref) => { var _a; useImperativeHandle(ref, () => ({ closeFinder, uploadImage })); useEffect(() => { return () => { closeFinder(); }; }, []); const [mediaLibraryVisible, setMediaLibraryVisible] = useState(false); const [extendModal, setExtendModal] = useState(null); const finderRef = useRef(null); const getControlTypeClassName = (data) => { let className = ''; const { type, command } = data; if (type === 'inline-style' && selectionHasInlineStyle(editorState, command)) { className += ' active'; } else if (type === 'block-type' && getSelectionBlockType(editorState) === command) { className += ' active'; } else if (type === 'entity' && getSelectionEntityType(editorState) === command) { className += ' active'; } return className; }; const applyControl = (command, type, data = {}) => { const hookCommand = command; if (type === 'inline-style') { const exclusiveInlineStyle = exclusiveInlineStyles[hookCommand]; if (exclusiveInlineStyle && selectionHasInlineStyle(editorState, exclusiveInlineStyle)) { editorState = toggleSelectionInlineStyle(editorState, exclusiveInlineStyle); } onChange(toggleSelectionInlineStyle(editorState, hookCommand)); } if (type === 'block-type') { onChange(toggleSelectionBlockType(editorState, hookCommand)); } if (type === 'entity') { onChange(toggleSelectionEntity(editorState, { type: hookCommand, mutability: data.mutability || 'MUTABLE', data: data.data || {} })); } if (type === 'editor-method' && commands[hookCommand]) { commands[hookCommand](); } }; const openFinder = () => { setMediaLibraryVisible(true); }; const insertMedias_ = (medias) => { var _a; onChange(insertMedias(editorState, medias)); onRequestFocus(); (_a = media.onInsert) === null || _a === void 0 ? void 0 : _a.call(media, medias); closeFinder(); }; const closeFinder = () => { var _a; (_a = media.onCancel) === null || _a === void 0 ? void 0 : _a.call(media); setMediaLibraryVisible(false); }; const uploadImage = useCallback((file, callback) => { var _a; (_a = finderRef.current) === null || _a === void 0 ? void 0 : _a.uploadImage(file, callback); }, [(_a = finderRef.current) === null || _a === void 0 ? void 0 : _a.uploadImage]); const preventDefault = (event) => { const tagName = event.target.tagName.toLowerCase(); if (tagName !== 'input' && tagName !== 'label') { event.preventDefault(); } }; const currentBlockType = getSelectionBlockType(editorState); const commonProps = useMemo(() => ({ editorId, editorState, getContainerNode, onChange, onRequestFocus }), [editorId, editorState, getContainerNode, onChange, onRequestFocus]); const language = useLanguage(); const editorControlMap = useMemo(() => getEditorControlMap(language, isFullscreen, mode), [language, isFullscreen, mode]); const parsedExtendControls = useMemo(() => { return extendControls.map((item) => typeof item === 'function' ? item(commonProps) : item); }, [extendControls, commonProps]); const allControls = useMemo(() => { return mergeControls(controls, parsedExtendControls); }, [controls, extendControls]); const renderedControlList = useMemo(() => { const keySet = new Set(); return allControls .filter((item) => { const itemKey = typeof item === 'string' ? item : item === null || item === void 0 ? void 0 : item.key; if (typeof itemKey !== 'string' || itemKey.length === 0 || keySet.has(itemKey)) { return false; } keySet.add(itemKey); return true; }) .map((item) => { return [item, uuidv4()]; }); }, [allControls]); const isControlDisabled = (control) => { if (control.disabled) { return true; } if (mode === 'html' && control.key !== 'html') { return true; } if (control.command === 'undo') { return editorState.getUndoStack().size === 0; } else if (control.command === 'redo') { return editorState.getRedoStack().size === 0; } return false; }; return (React.createElement("div", { className: cls(`kedao-controlbar ${className || ''}`), style: style, onMouseDown: preventDefault, role: "button", tabIndex: 0 }, renderedControlList.map(([item, key]) => { const itemKey = typeof item === 'string' ? item : item.key; if (itemKey.toLowerCase() === 'separator') { return React.createElement("span", { key: key, className: cls('separator-line') }); } let controlItem = editorControlMap[itemKey.toLowerCase()]; if (typeof item !== 'string') { controlItem = Object.assign(Object.assign({}, controlItem), item); } if (!controlItem) { return null; } const disabled = isControlDisabled(controlItem); const controlStateProps = { disabled }; if (controlItem.type === 'headings') { return (React.createElement(HeadingPicker, Object.assign({ key: key, current: currentBlockType }, commonProps, controlStateProps, { onChange: (command) => applyControl(command, 'block-type') }))); } if (controlItem.type === 'text-color') { return (React.createElement(TextColorPicker, Object.assign({ key: key, enableBackgroundColor: textBackgroundColor }, commonProps, controlStateProps))); } if (controlItem.type === 'font-size') { return (React.createElement(FontSizePicker, Object.assign({ key: key, defaultCaption: controlItem.title }, commonProps, controlStateProps))); } if (controlItem.type === 'line-height') { return (React.createElement(LineHeightPicker, Object.assign({ key: key, defaultCaption: controlItem.title }, commonProps, controlStateProps))); } if (controlItem.type === 'letter-spacing') { return (React.createElement(LetterSpacingPicker, Object.assign({ key: key, defaultCaption: controlItem.title }, commonProps, controlStateProps))); } if (controlItem.type === 'text-indent') { return (React.createElement(TextIndent, Object.assign({ key: key }, commonProps, controlStateProps))); } if (controlItem.type === 'font-family') { return (React.createElement(FontFamilyPicker, Object.assign({ key: key, defaultCaption: controlItem.title }, commonProps, controlStateProps))); } if (controlItem.type === 'emoji') { return (React.createElement(EmojiPicker, Object.assign({ key: key, defaultCaption: controlItem.text }, commonProps, controlStateProps))); } if (controlItem.type === 'link') { return (React.createElement(LinkEditor, Object.assign({ key: key, allowInsertLinkText: allowInsertLinkText, onChange: onChange, onRequestFocus: onRequestFocus }, commonProps, controlStateProps))); } if (controlItem.type === 'text-align') { return (React.createElement(TextAlign, Object.assign({ key: key }, commonProps, controlStateProps))); } if (controlItem.type === 'media') { if (!media.image && !media.video && !media.audio) { return null; } return (React.createElement(Button, Object.assign({ type: "button", key: key, "data-title": controlItem.title, className: cls('media'), onClick: openFinder }, controlStateProps), controlItem.text)); } if (isDropDownControl(controlItem)) { return (React.createElement(DropDown, Object.assign({ key: key, className: cls(`extend-control-item ${controlItem.className || ''}`), caption: controlItem.text, htmlCaption: controlItem.html, showArrow: controlItem.showArrow, title: controlItem.title, arrowActive: controlItem.arrowActive, autoHide: controlItem.autoHide, ref: controlItem.ref }, commonProps, controlStateProps), controlItem.component)); } if (isModalControl(controlItem)) { return (React.createElement(Button, Object.assign({ type: "button", key: key, "data-title": controlItem.title, className: cls(`extend-control-item ${controlItem.className || ''}`), dangerouslySetInnerHTML: controlItem.html ? { __html: controlItem.html } : null, onClick: (event) => { const { modal, onClick } = controlItem; if (modal === null || modal === void 0 ? void 0 : modal.id) { setExtendModal(modal); } onClick === null || onClick === void 0 ? void 0 : onClick(event); } }, controlStateProps), !controlItem.html ? controlItem.text : null)); } if (controlItem.type === 'component') { return (React.createElement("div", { key: key, className: cls(`component-wrapper ${controlItem.className || ''}`) }, typeof controlItem.component === 'function' ? React.createElement(controlItem.component, Object.assign(Object.assign({}, commonProps), controlStateProps)) : controlItem.component)); } if (isButtonControl(controlItem)) { return (React.createElement(Button, Object.assign({ type: "button", key: key, "data-title": controlItem.title, className: cls(controlItem.className || ''), dangerouslySetInnerHTML: controlItem.html ? { __html: controlItem.html } : null, onClick: (event) => { var _a, _b; return (_b = (_a = controlItem).onClick) === null || _b === void 0 ? void 0 : _b.call(_a, event); } }, controlStateProps), !controlItem.html ? controlItem.text : null)); } if (controlItem) { return (React.createElement(Button, Object.assign({ type: "button", key: key, "data-title": controlItem.title, className: cls(getControlTypeClassName({ type: controlItem.type, command: controlItem.command })), onClick: () => applyControl(controlItem.command, controlItem.type, controlItem.data) }, controlStateProps), controlItem.text)); } return null; }), mediaLibraryVisible && (React.createElement(Modal, { title: language.controls.mediaLibirary, width: 640, showFooter: false, onClose: closeFinder, visible: mediaLibraryVisible }, React.createElement(Finder, Object.assign({ ref: finderRef }, media, { onCancel: closeFinder, onInsert: insertMedias_ })))), extendModal && React.createElement(Modal, Object.assign({ key: extendModal.id }, extendModal)))); }); export default ControlBar;