kedao
Version:
Rich Text Editor Based On Draft.js
492 lines (491 loc) • 23.8 kB
JavaScript
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;