UNPKG

kedao

Version:

Rich Text Editor Based On Draft.js

497 lines (496 loc) 24.5 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, { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import { toggleSelectionIndent, insertHorizontalLine, undo, redo, insertHTML, increaseSelectionIndent, getSelectedBlocks, insertMedias, removeBlock, getSelectionBlock, removeSelectionInlineStyles, getSelectionBlockType, decreaseSelectionIndent, toggleSelectionBlockType, insertText, handleNewLine, handleKeyCommand as defaultHandleKeyCommand, clear, convertRawToEditorState, convertHTMLToEditorState, convertEditorStateToHTML } from '../utils'; import { Editor, RichUtils, Modifier, EditorState, getDefaultKeyBinding, KeyBindingUtil, ContentState } from 'draft-js'; import mergeClassNames from 'merge-class-names'; import { Provider as JotaiProvider, useSetAtom } from 'jotai'; import { getBlockRendererFn, getBlockRenderMap, getBlockStyleFn, getCustomStyleMap, getCustomStyleFn, getDecorators } from '../utils/renderers'; import ControlBar from '../components/ControlBar'; import 'draft-js/dist/Draft.css'; import styles from "./style.module.css"; import getFragmentFromSelection from 'draft-js/lib/getFragmentFromSelection'; import { defaultControls, defaultFontFamilies, defaultImageControls } from '../constants'; import { langAtom } from '../states'; const cls = classNameParser(styles); const langLoaders = { en: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/en')).default; }), jpn: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/jpn')).default; }), kr: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/kr')).default; }), pl: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/pl')).default; }), ru: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/ru')).default; }), tr: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/tr')).default; }), 'zh-hant': () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/zh-hant')).default; }), zh: () => __awaiter(void 0, void 0, void 0, function* () { return (yield import('../i18n/zh')).default; }) }; const defaultMedia = { pasteImage: true, imagePasteLimit: 5, image: true, video: true, audio: true, uploadFn: null, validateFn: null, onBeforeDeselect: null, onDeselect: null, onBeforeSelect: null, onSelect: null, onBeforeRemove: null, onRemove: null, onCancel: null, onFileSelect: null, onBeforeInsert: null, onInsert: null, onChange: null, accepts: { image: 'image/png,image/jpeg,image/gif,image/webp,image/apng,image/svg', video: 'video/mp4', audio: 'audio/mp3' }, externals: { audio: true, video: true, image: true, embed: true } }; const unitExportFn = (value, type) => type === 'line-height' ? value : `${value}px`; export const createStateFromContent = (content, options = {}) => { const customOptions = Object.assign({}, options); customOptions.unitExportFn = customOptions.unitExportFn || unitExportFn; let editorState = null; if (content instanceof EditorState) { editorState = content; } if (typeof content === 'object' && content && content.blocks && content.entityMap) { editorState = convertRawToEditorState(content, getDecorators()); } if (typeof content === 'string') { try { if (/^(-)?\d+$/.test(content)) { editorState = convertHTMLToEditorState(content, getDecorators(), customOptions, 'create'); } else { editorState = createStateFromContent(JSON.parse(content), customOptions); } } catch (error) { editorState = convertHTMLToEditorState(content, getDecorators(), customOptions, 'create'); } } else if (typeof content === 'number') { editorState = convertHTMLToEditorState(Number(content) .toLocaleString() .replace(/,/g, ''), getDecorators(), customOptions, 'create'); } else { editorState = EditorState.createEmpty(getDecorators()); } return editorState; }; const defaultExtendControls = []; const defaultExtendAtomics = []; const defaultExcludeControls = []; const defaultConverts = { unitExportFn }; const KedaoEditor = ({ controls = defaultControls, language: locale = 'zh', excludeControls = defaultExcludeControls, handlePastedText, extendControls = defaultExtendControls, extendAtomics = defaultExtendAtomics, componentBelowControlBar = null, media = defaultMedia, imageControls = defaultImageControls, imageResizable = true, imageEqualRatio = true, codeTabIndents = 2, textBackgroundColor = true, allowInsertLinkText = false, converts = defaultConverts, stripPastedStyles = false, className = '', handleKeyCommand, onSave, onBlur, onDelete, onFocus, handleReturn, handleBeforeInput, handleDroppedFiles, blockStyleFn, blockRendererFn, customStyleMap, handlePastedFiles, blockRenderMap, customStyleFn, onFullscreen, placeholder, readOnly, disabled, style, controlBarClassName = '', controlBarStyle, contentClassName = '', contentStyle, defaultValue, value, editorId, keyBindingFn, id, onChange, fixPlaceholder = false }) => { var _a; const draftInstanceRef = useRef(null); const [editorLocked, setEditorLocked] = useState(null); const editorDecoratorsRef = useRef(getDecorators()); const controlBarInstanceRef = useRef(null); const [isLiving, setIsLiving] = useState(false); const setLanuage = useSetAtom(langAtom); const [mode, setMode] = useState('richtext'); const [html, setHtml] = useState(''); const defaultEditorState = useMemo(() => (defaultValue || value) instanceof EditorState ? defaultValue || value : EditorState.createEmpty(editorDecoratorsRef.current), []); const [editorState, setEditorState] = useState(defaultEditorState); const [isFullscreen, setIsFullscreen] = useState(false); const containerRef = useRef(null); useEffect(() => { const setupLang = () => __awaiter(void 0, void 0, void 0, function* () { const loader = langLoaders[locale]; if (loader) { try { const language = yield loader(); setLanuage(language); } catch (error) { console.error(error); } } }); setupLang().catch(console.error); }, [locale]); const getConvertOptions = () => { const result = Object.assign(Object.assign({ unitExportFn }, converts), { fontFamilies: defaultFontFamilies }); return result; }; const convertOptions = useMemo(getConvertOptions, [editorId, id, converts]); useEffect(() => { if (isLiving) { setEditorState(value); } }, [value]); useEffect(() => { setIsLiving(true); return () => { setIsLiving(false); }; }, []); const handleChange = useCallback((editorState, callback) => { let newEditorState = editorState; if (!(editorState instanceof EditorState)) { newEditorState = EditorState.set(editorState, { decorator: editorDecoratorsRef.current }); } setEditorState(newEditorState); onChange === null || onChange === void 0 ? void 0 : onChange(newEditorState); callback === null || callback === void 0 ? void 0 : callback(newEditorState); }, [editorDecoratorsRef.current, onChange]); const setValue = useCallback((editorState, callback) => { return handleChange(editorState, callback); }, [handleChange]); const forceRender = () => { const selectionState = editorState.getSelection(); setValue(EditorState.set(editorState, { decorator: editorDecoratorsRef.current }), () => { setValue(EditorState.forceSelection(editorState, selectionState)); }); }; const keyCommandHandlers = useCallback((command, editorState) => { if ((handleKeyCommand === null || handleKeyCommand === void 0 ? void 0 : handleKeyCommand(command, editorState)) === 'handled') { return 'handled'; } if (command === 'kedao-save') { onSave === null || onSave === void 0 ? void 0 : onSave(editorState); return 'handled'; } const allowIndent = controls.some(ctrl => { return typeof ctrl === 'string' ? ctrl === 'text-indent' : ctrl.type === 'text-indent'; }) && !excludeControls.includes('text-indent'); const cursorStart = editorState.getSelection().getStartOffset(); const cursorEnd = editorState.getSelection().getEndOffset(); const cursorIsAtFirst = cursorStart === 0 && cursorEnd === 0; if (command === 'backspace') { if (onDelete && !(onDelete === null || onDelete === void 0 ? void 0 : onDelete(editorState))) { return 'handled'; } const blockType = getSelectionBlockType(editorState); if (allowIndent && cursorIsAtFirst && blockType !== 'code-block') { setValue(decreaseSelectionIndent(editorState)); } } if (command === 'tab') { const blockType = getSelectionBlockType(editorState); if (blockType === 'code-block') { setValue(insertText(editorState, ' '.repeat(codeTabIndents))); return 'handled'; } if (blockType === 'ordered-list-item' || blockType === 'unordered-list-item') { const newEditorState = RichUtils.onTab(event, editorState, 4); if (newEditorState !== editorState) { setValue(newEditorState); } return 'handled'; } if (blockType !== 'atomic' && allowIndent && cursorIsAtFirst) { setValue(increaseSelectionIndent(editorState)); return 'handled'; } } const nextEditorState = defaultHandleKeyCommand(editorState, command); if (nextEditorState) { setValue(nextEditorState); return 'handled'; } return 'not-handled'; }, [setValue, handleKeyCommand, onSave, onDelete, controls, excludeControls, codeTabIndents]); const handleFocus = useCallback(() => { onFocus === null || onFocus === void 0 ? void 0 : onFocus(editorState); }, [onFocus]); const handleBlur = useCallback(() => { onBlur === null || onBlur === void 0 ? void 0 : onBlur(editorState); }, [onBlur]); const requestFocus = useCallback(() => { setTimeout(() => { var _a; return (_a = draftInstanceRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 0); }, [draftInstanceRef.current]); const handleReturn_ = useCallback((event, editorState) => { if ((handleReturn === null || handleReturn === void 0 ? void 0 : handleReturn(event, editorState)) === 'handled') { return 'handled'; } const currentBlock = getSelectionBlock(editorState); const currentBlockType = currentBlock.getType(); if (currentBlockType === 'unordered-list-item' || currentBlockType === 'ordered-list-item') { if (currentBlock.getLength() === 0) { setValue(toggleSelectionBlockType(editorState, 'unstyled')); return 'handled'; } return 'not-handled'; } if (currentBlockType === 'code-block') { if (event.which === 13 && (event.getModifierState('Shift') || event.getModifierState('Alt') || event.getModifierState('Control'))) { setValue(toggleSelectionBlockType(editorState, 'unstyled')); return 'handled'; } return 'not-handled'; } if (currentBlockType === 'blockquote') { if (event.which === 13) { if (event.getModifierState('Shift') || event.getModifierState('Alt') || event.getModifierState('Control')) { // eslint-disable-next-line no-param-reassign event.which = 0; } else { setValue(RichUtils.insertSoftNewline(editorState)); return 'handled'; } } } const nextEditorState = handleNewLine(editorState, event); if (nextEditorState) { setValue(nextEditorState); return 'handled'; } return 'not-handled'; }, [setValue, handleReturn, handleNewLine]); const handleBeforeInput_ = useCallback((chars, editorState) => { if ((handleBeforeInput === null || handleBeforeInput === void 0 ? void 0 : handleBeforeInput(chars, editorState)) === 'handled') { return 'handled'; } return 'not-handled'; }, []); const handleDrop = useCallback((selectionState, dataTransfer) => { if (readOnly || disabled) { return 'handled'; } if (window.__KEDAO_DRAGING__IMAGE__) { let nextEditorState = EditorState.forceSelection(editorState, selectionState); nextEditorState = insertMedias(nextEditorState, [ window.__KEDAO_DRAGING__IMAGE__.mediaData ]); nextEditorState = removeBlock(nextEditorState, window.__KEDAO_DRAGING__IMAGE__.block, nextEditorState.getSelection()); window.__KEDAO_DRAGING__IMAGE__ = null; setEditorLocked(true); setValue(nextEditorState); return 'handled'; } if (!dataTransfer || !dataTransfer.getText()) { return 'handled'; } return 'not-handled'; }, []); const handleFiles = useCallback(files => { const { pasteImage, validateFn, imagePasteLimit } = Object.assign(Object.assign({}, defaultMedia), media); const upload = file => { var _a; (_a = controlBarInstanceRef.current) === null || _a === void 0 ? void 0 : _a.uploadImage(file, image => { if (isLiving) { setValue(insertMedias(editorState, [image])); } }); }; if (pasteImage) { files.slice(0, imagePasteLimit).forEach(file => { if (file && file.type.indexOf('image') > -1 && controlBarInstanceRef.current) { const validateResult = validateFn ? validateFn(file) : true; if (validateResult instanceof Promise) { validateResult .then(() => { upload(file); }) .catch(console.error); } else if (validateResult) { upload(file); } } }); } if (files[0] && files[0].type.indexOf('image') > -1 && pasteImage) { return 'handled'; } return 'not-handled'; }, [!!controlBarInstanceRef.current, media, (_a = controlBarInstanceRef.current) === null || _a === void 0 ? void 0 : _a.uploadImage]); const handleDroppedFiles_ = useCallback((selectionState, files) => { if ((handleDroppedFiles === null || handleDroppedFiles === void 0 ? void 0 : handleDroppedFiles(selectionState, files)) === 'handled') { return 'handled'; } return handleFiles(files); }, [handleFiles, handleDroppedFiles]); const handlePastedFiles_ = useCallback(files => { if ((handlePastedFiles === null || handlePastedFiles === void 0 ? void 0 : handlePastedFiles(files)) === 'handled') { return 'handled'; } return handleFiles(files); }, [handleFiles, handlePastedFiles]); const handleCopyContent = event => { const blockMap = getFragmentFromSelection(editorState); if (blockMap === null || blockMap === void 0 ? void 0 : blockMap.toArray) { try { const tempContentState = ContentState.createFromBlockArray(blockMap.toArray()); const tempEditorState = EditorState.createWithContent(tempContentState); const clipboardData = event.clipboardData || window.clipboardData || event.originalEvent.clipboardData; const html = convertEditorStateToHTML(tempEditorState, convertOptions); const text = tempEditorState.getCurrentContent().getPlainText(); clipboardData.setData('text/html', html); clipboardData.setData('text/plain', text); event.preventDefault(); } catch (error) { console.warn(error); } } }; const handlePastedText_ = useCallback((text, html, editorState) => { if ((handlePastedText === null || handlePastedText === void 0 ? void 0 : handlePastedText(text, html, editorState)) === 'handled') { return 'handled'; } if (!html || stripPastedStyles) { return 'not-handled'; } setValue(insertHTML(editorState, convertOptions, html, 'paste')); return 'handled'; }, [stripPastedStyles, setValue]); const handleCompositionStart = () => { const selectedBlocks = getSelectedBlocks(editorState); if (selectedBlocks && selectedBlocks.length > 1) { const nextEditorState = EditorState.push(editorState, Modifier.removeRange(editorState.getCurrentContent(), editorState.getSelection(), 'backward'), 'remove-range'); setValue(nextEditorState); } }; editorId = editorId || id; controls = controls.filter(item => !excludeControls.includes(item)); const controlBarMedia = useMemo(() => { const result = Object.assign(Object.assign(Object.assign({}, defaultMedia), media), { accepts: Object.assign(Object.assign({}, defaultMedia.accepts), media === null || media === void 0 ? void 0 : media.accepts) }); if (!result.uploadFn) { result.video = false; result.audio = false; } return result; }, [media]); const commands = useMemo(() => ({ undo: () => { setValue(undo(editorState)); }, redo: () => { setValue(redo(editorState)); }, removeSelectionInlineStyles: () => { setValue(removeSelectionInlineStyles(editorState)); }, insertHorizontalLine: () => { setValue(insertHorizontalLine(editorState)); }, clearEditorContent: () => { setValue(clear(editorState), (editorState) => { setValue(toggleSelectionIndent(editorState, 0)); }); }, toggleFullscreen: () => { let newValue = null; setIsFullscreen(v => { newValue = !v; return newValue; }); onFullscreen === null || onFullscreen === void 0 ? void 0 : onFullscreen(newValue); }, toggleHtml: () => { setMode(oldMode => oldMode === 'html' ? 'richtext' : 'html'); setHtml(convertEditorStateToHTML(editorState)); } }), [onFullscreen, editorState]); const getContainerNode = useCallback(() => containerRef.current, [ containerRef.current ]); const blockRendererFn_ = getBlockRendererFn({ value: editorState, onChange: setValue, readOnly: readOnly || disabled, imageControls, imageResizable, imageEqualRatio, getContainerNode, refresh: () => forceRender(), lock: setEditorLocked, extendAtomics, editorId }, blockRendererFn); const blockRenderMap_ = useMemo(() => getBlockRenderMap(blockRenderMap), [blockRenderMap]); const blockStyleFn_ = useMemo(() => getBlockStyleFn(blockStyleFn), [blockStyleFn]); const customStyleMap_ = useMemo(() => getCustomStyleMap(customStyleMap), [customStyleMap]); const customStyleFn_ = useMemo(() => getCustomStyleFn({ fontFamilies: defaultFontFamilies, unitExportFn, customStyleFn: customStyleFn }), [customStyleFn]); const keyBindingFn_ = useCallback(event => { if (event.keyCode === 83 && (KeyBindingUtil.hasCommandModifier(event) || KeyBindingUtil.isCtrlKeyCommand(event))) { return 'kedao-save'; } if (keyBindingFn) { return keyBindingFn(event) || getDefaultKeyBinding(event); } return getDefaultKeyBinding(event); }, [keyBindingFn]); const mixedProps = {}; if (editorLocked || disabled || readOnly) { mixedProps.readOnly = true; } if (placeholder && fixPlaceholder && !editorState.getCurrentContent().hasText() && editorState .getCurrentContent() .getFirstBlock() .getType() !== 'unstyled') { placeholder = ''; } const memoControls = useMemo(() => controls, [controls === null || controls === void 0 ? void 0 : controls.join(',')]); const renderEditor = () => { if (mode === 'html') { return (React.createElement("div", { className: cls('kedao-html-container') }, React.createElement("textarea", { value: html, readOnly: true, className: cls('kedao-html') }))); } return (React.createElement(Editor, { ref: draftInstanceRef, editorState: editorState, handleKeyCommand: keyCommandHandlers, handleReturn: handleReturn_, handleBeforeInput: handleBeforeInput_, handleDrop: handleDrop, handleDroppedFiles: handleDroppedFiles_, handlePastedText: handlePastedText_, handlePastedFiles: handlePastedFiles_, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, blockRenderMap: blockRenderMap_, blockRendererFn: blockRendererFn_, blockStyleFn: blockStyleFn_, customStyleMap: customStyleMap_, customStyleFn: customStyleFn_, keyBindingFn: keyBindingFn_, placeholder: placeholder, stripPastedStyles: stripPastedStyles, readOnly: editorLocked || disabled || readOnly })); }; return (React.createElement("div", { style: style, ref: containerRef, className: cls(mergeClassNames('kedao-container', className, disabled && 'disabled', readOnly && 'read-only', isFullscreen && 'fullscreen')) }, React.createElement(ControlBar, { ref: controlBarInstanceRef, editorState: editorState, getContainerNode: getContainerNode, className: cls(controlBarClassName), style: controlBarStyle, editorId: editorId, media: controlBarMedia, controls: memoControls, extendControls: extendControls, textBackgroundColor: textBackgroundColor, allowInsertLinkText: allowInsertLinkText, isFullscreen: isFullscreen, onChange: setValue, onRequestFocus: requestFocus, commands: commands, mode: mode }), componentBelowControlBar, React.createElement("div", { onCompositionStart: handleCompositionStart, className: cls(`kedao-content ${contentClassName}`), onCopy: handleCopyContent, style: contentStyle }, renderEditor()))); }; const JotaiWrapper = props => { return (React.createElement(JotaiProvider, null, React.createElement(KedaoEditor, Object.assign({}, props)))); }; export { EditorState }; export default JotaiWrapper;