UNPKG

kedao

Version:

Rich Text Editor Based On Draft.js

1,251 lines (1,250 loc) 50 kB
/* eslint-disable react/display-name */ 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()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import React from 'react'; import { Modifier, EditorState, SelectionState, RichUtils, CharacterMetadata, AtomicBlockUtils, convertFromRaw, convertToRaw, KeyBindingUtil } from 'draft-js'; import Immutable from 'immutable'; import { namedColors, defaultFontFamilies } from '../constants'; import { convertToHTML, convertFromHTML } from 'draft-convert'; const strictBlockTypes = ['atomic']; export const registerStrictBlockType = (blockType) => { !strictBlockTypes.includes(blockType) && strictBlockTypes.push(blockType); }; export const selectionContainsBlockType = (editorState, blockType) => { return getSelectedBlocks(editorState).find((block) => block.getType() === blockType); }; export const selectionContainsStrictBlock = (editorState) => { return getSelectedBlocks(editorState).find((block) => ~strictBlockTypes.indexOf(block.getType())); }; export const selectBlock = (editorState, block) => { const blockKey = block.getKey(); return EditorState.forceSelection(editorState, new SelectionState({ anchorKey: blockKey, anchorOffset: 0, focusKey: blockKey, focusOffset: block.getLength() })); }; export const selectNextBlock = (editorState, block) => { const nextBlock = editorState .getCurrentContent() .getBlockAfter(block.getKey()); return nextBlock ? selectBlock(editorState, nextBlock) : editorState; }; export const removeBlock = (editorState, block, lastSelection = null) => { let nextContentState; const blockKey = block.getKey(); nextContentState = Modifier.removeRange(editorState.getCurrentContent(), new SelectionState({ anchorKey: blockKey, anchorOffset: 0, focusKey: blockKey, focusOffset: block.getLength() }), 'backward'); nextContentState = Modifier.setBlockType(nextContentState, nextContentState.getSelectionAfter(), 'unstyled'); const nextEditorState = EditorState.push(editorState, nextContentState, 'remove-range'); return EditorState.forceSelection(nextEditorState, lastSelection || nextContentState.getSelectionAfter()); }; export const getSelectionBlock = (editorState) => { return editorState .getCurrentContent() .getBlockForKey(editorState.getSelection().getAnchorKey()); }; export const updateEachCharacterOfSelection = (editorState, // TODO: check type of character callback) => { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); const contentBlocks = contentState.getBlockMap(); const selectedBlocks = getSelectedBlocks(editorState); if (selectedBlocks.length === 0) { return editorState; } const startKey = selectionState.getStartKey(); const startOffset = selectionState.getStartOffset(); const endKey = selectionState.getEndKey(); const endOffset = selectionState.getEndOffset(); const nextContentBlocks = contentBlocks.map((block) => { if (!selectedBlocks.includes(block)) { return block; } const blockKey = block.getKey(); const charactersList = block.getCharacterList(); let nextCharactersList = null; if (blockKey === startKey && blockKey === endKey) { nextCharactersList = charactersList .map((character, index) => { if (index >= startOffset && index < endOffset) { return callback(character); } return character; }) .toList(); } else if (blockKey === startKey) { nextCharactersList = charactersList .map((character, index) => { if (index >= startOffset) { return callback(character); } return character; }) .toList(); } else if (blockKey === endKey) { nextCharactersList = charactersList .map((character, index) => { if (index < endOffset) { return callback(character); } return character; }) .toList(); } else { nextCharactersList = charactersList .map((character) => { return callback(character); }) .toList(); } return block.merge({ characterList: nextCharactersList }); }); return EditorState.push(editorState, contentState.merge({ blockMap: nextContentBlocks, selectionBefore: selectionState, selectionAfter: selectionState }), 'update-selection-character-list'); }; export const getSelectedBlock = (editorState) => { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); const startKey = selectionState.getStartKey(); const startingBlock = contentState.getBlockForKey(startKey); return startingBlock; }; export const getSelectedBlocks = (editorState) => { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); const startKey = selectionState.getStartKey(); const endKey = selectionState.getEndKey(); const isSameBlock = startKey === endKey; const startingBlock = contentState.getBlockForKey(startKey); const selectedBlocks = [startingBlock]; if (!isSameBlock) { let blockKey = startKey; while (blockKey !== endKey) { const nextBlock = contentState.getBlockAfter(blockKey); if (nextBlock) { selectedBlocks.push(nextBlock); blockKey = nextBlock.getKey(); } } } return selectedBlocks; }; export const setSelectionBlockData = (editorState, blockData, override = false) => { const newBlockData = override ? blockData : Object.assign({}, getSelectionBlockData(editorState).toJS(), blockData); Object.keys(newBlockData).forEach((key) => { // eslint-disable-next-line no-prototype-builtins if (newBlockData.hasOwnProperty(key) && newBlockData[key] === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete newBlockData[key]; } }); return setBlockData(editorState, newBlockData); }; export const getSelectionBlockData = (editorState, name) => { const blockData = getSelectionBlock(editorState).getData(); return name ? blockData.get(name) : blockData; }; export const getSelectionBlockType = (editorState) => { return getSelectionBlock(editorState).getType(); }; export const getSelectionText = (editorState) => { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); if (selectionState.isCollapsed() || getSelectionBlockType(editorState) === 'atomic') { return ''; } const anchorKey = selectionState.getAnchorKey(); const currentContentBlock = contentState.getBlockForKey(anchorKey); const start = selectionState.getStartOffset(); const end = selectionState.getEndOffset(); return currentContentBlock.getText().slice(start, end); }; export const toggleSelectionBlockType = (editorState, blockType) => { if (selectionContainsStrictBlock(editorState)) { return editorState; } return RichUtils.toggleBlockType(editorState, blockType); }; export const getSelectionEntityType = (editorState) => { const entityKey = getSelectionEntity(editorState); if (entityKey) { const entity = editorState.getCurrentContent().getEntity(entityKey); return entity ? entity.getType() : null; } return null; }; export const getSelectionEntityData = (editorState, type) => { const entityKey = getSelectionEntity(editorState); if (entityKey) { const entity = editorState.getCurrentContent().getEntity(entityKey); if (entity && entity.getType() === type) { return entity.getData(); } else { return {}; } } else { return {}; } }; export const toggleSelectionEntity = (editorState, entity) => { const contentState = editorState.getCurrentContent(); const selectionState = editorState.getSelection(); if (selectionState.isCollapsed() || getSelectionBlockType(editorState) === 'atomic') { return editorState; } if (!entity || !entity.type || getSelectionEntityType(editorState) === entity.type) { return EditorState.push(editorState, Modifier.applyEntity(contentState, selectionState, null), 'apply-entity'); } try { const nextContentState = contentState.createEntity(entity.type, entity.mutability, entity.data); const entityKey = nextContentState.getLastCreatedEntityKey(); const nextEditorState = EditorState.set(editorState, { currentContent: nextContentState }); return EditorState.push(nextEditorState, Modifier.applyEntity(nextContentState, selectionState, entityKey), 'apply-entity'); } catch (error) { console.warn(error); return editorState; } }; export const toggleSelectionLink = (editorState, href, target) => { const contentState = editorState.getCurrentContent(); const selectionState = editorState.getSelection(); const entityData = { href, target }; if (selectionState.isCollapsed() || getSelectionBlockType(editorState) === 'atomic') { return editorState; } if (href === false) { return RichUtils.toggleLink(editorState, selectionState, null); } if (href === null) { delete entityData.href; } try { const nextContentState = contentState.createEntity('LINK', 'MUTABLE', entityData); const entityKey = nextContentState.getLastCreatedEntityKey(); let nextEditorState = EditorState.set(editorState, { currentContent: nextContentState }); nextEditorState = RichUtils.toggleLink(nextEditorState, selectionState, entityKey); nextEditorState = EditorState.forceSelection(nextEditorState, selectionState.merge({ anchorOffset: selectionState.getEndOffset(), focusOffset: selectionState.getEndOffset() })); nextEditorState = EditorState.push(nextEditorState, Modifier.insertText(nextEditorState.getCurrentContent(), nextEditorState.getSelection(), ''), 'insert-text'); return nextEditorState; } catch (error) { console.warn(error); return editorState; } }; export const getSelectionInlineStyle = (editorState) => { return editorState.getCurrentInlineStyle(); }; export const selectionHasInlineStyle = (editorState, style) => { return getSelectionInlineStyle(editorState).has(style.toUpperCase()); }; export const toggleSelectionInlineStyle = (editorState, style, prefix = '') => { let nextEditorState = editorState; style = prefix + style.toUpperCase(); if (prefix) { nextEditorState = updateEachCharacterOfSelection(nextEditorState, (characterMetadata) => { return characterMetadata .toJS() .style.reduce((characterMetadata, characterStyle) => { if (characterStyle.indexOf(prefix) === 0 && style !== characterStyle) { return CharacterMetadata.removeStyle(characterMetadata, characterStyle); } else { return characterMetadata; } }, characterMetadata); }); } return RichUtils.toggleInlineStyle(nextEditorState, style); }; export const removeSelectionInlineStyles = (editorState) => { return updateEachCharacterOfSelection(editorState, (characterMetadata) => { return characterMetadata.merge({ style: Immutable.OrderedSet([]) }); }); }; export const toggleSelectionAlignment = (editorState, alignment) => { return setSelectionBlockData(editorState, { textAlign: getSelectionBlockData(editorState, 'textAlign') !== alignment ? alignment : undefined }); }; export const toggleSelectionIndent = (editorState, textIndent, maxIndent = 6) => { return textIndent < 0 || textIndent > maxIndent || isNaN(textIndent) ? editorState : setSelectionBlockData(editorState, { textIndent: textIndent || undefined }); }; export const increaseSelectionIndent = (editorState, maxIndent = 6) => { const currentIndent = getSelectionBlockData(editorState, 'textIndent') || 0; return toggleSelectionIndent(editorState, currentIndent + 1, maxIndent); }; export const decreaseSelectionIndent = (editorState, maxIndent) => { const currentIndent = getSelectionBlockData(editorState, 'textIndent') || 0; return toggleSelectionIndent(editorState, currentIndent - 1, maxIndent); }; export const toggleSelectionColor = (editorState, color) => { return toggleSelectionInlineStyle(editorState, color.replace('#', ''), 'COLOR-'); }; export const toggleSelectionBackgroundColor = (editorState, color) => { return toggleSelectionInlineStyle(editorState, color.replace('#', ''), 'BGCOLOR-'); }; export const toggleSelectionFontSize = (editorState, fontSize) => { return toggleSelectionInlineStyle(editorState, fontSize, 'FONTSIZE-'); }; export const toggleSelectionLineHeight = (editorState, lineHeight) => { return toggleSelectionInlineStyle(editorState, lineHeight, 'LINEHEIGHT-'); }; export const toggleSelectionFontFamily = (editorState, fontFamily) => { return toggleSelectionInlineStyle(editorState, fontFamily, 'FONTFAMILY-'); }; export const toggleSelectionLetterSpacing = (editorState, letterSpacing) => { return toggleSelectionInlineStyle(editorState, letterSpacing, 'LETTERSPACING-'); }; export const insertText = (editorState, text, inlineStyle, entity) => { const selectionState = editorState.getSelection(); const currentSelectedBlockType = getSelectionBlockType(editorState); if (currentSelectedBlockType === 'atomic') { return editorState; } let entityKey; let contentState = editorState.getCurrentContent(); if (entity === null || entity === void 0 ? void 0 : entity.type) { contentState = contentState.createEntity(entity.type, entity.mutability || 'MUTABLE', // entity.data || entityData entity.data); entityKey = contentState.getLastCreatedEntityKey(); } if (!selectionState.isCollapsed()) { return EditorState.push(editorState, Modifier.replaceText(contentState, selectionState, text, inlineStyle, entityKey), 'replace-text'); } else { return EditorState.push(editorState, Modifier.insertText(contentState, selectionState, text, inlineStyle, entityKey), 'insert-text'); } }; export const insertHTML = (editorState, options, htmlString, source) => { if (!htmlString) { return editorState; } const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); try { const { blockMap } = convertFromRaw(convertHTMLToRaw(htmlString, options, source)); return EditorState.push(editorState, Modifier.replaceWithFragment(contentState, selectionState, blockMap), 'insert-fragment'); } catch (error) { console.warn(error); return editorState; } }; export const insertAtomicBlock = (editorState, type, immutable = true, data = {}) => { if (selectionContainsStrictBlock(editorState)) { return insertAtomicBlock(selectNextBlock(editorState, getSelectionBlock(editorState)), type, immutable, data); } const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); if (!selectionState.isCollapsed() || getSelectionBlockType(editorState) === 'atomic') { return editorState; } const contentStateWithEntity = contentState.createEntity(type, immutable ? 'IMMUTABLE' : 'MUTABLE', data); const entityKey = contentStateWithEntity.getLastCreatedEntityKey(); const newEditorState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' '); return newEditorState; }; export const insertHorizontalLine = (editorState) => { return insertAtomicBlock(editorState, 'HR'); }; export const insertMedias = (editorState, medias = []) => { if (!medias.length) { return editorState; } return medias.reduce((editorState, media) => { // eslint-disable-next-line @typescript-eslint/naming-convention const { url, link, link_target, name, type, width, height, meta } = media; return insertAtomicBlock(editorState, type, true, { url, link, link_target, name, type, width, height, meta }); }, editorState); }; export const setMediaData = (editorState, entityKey, data) => { return EditorState.push(editorState, editorState.getCurrentContent().mergeEntityData(entityKey, data), 'change-block-data'); }; export const removeMedia = (editorState, mediaBlock) => { return removeBlock(editorState, mediaBlock); }; export const setMediaPosition = (editorState, // TODO: check type mediaBlock, position) => { const newPosition = {}; const { float, alignment } = position; if (typeof float !== 'undefined') { newPosition.float = mediaBlock.getData().get('float') === float ? null : float; } if (typeof alignment !== 'undefined') { newPosition.alignment = mediaBlock.getData().get('alignment') === alignment ? null : alignment; } return setSelectionBlockData(selectBlock(editorState, mediaBlock), newPosition); }; export const clear = (editorState) => { const contentState = editorState.getCurrentContent(); const firstBlock = contentState.getFirstBlock(); const lastBlock = contentState.getLastBlock(); const allSelected = new SelectionState({ anchorKey: firstBlock.getKey(), anchorOffset: 0, focusKey: lastBlock.getKey(), focusOffset: lastBlock.getLength(), hasFocus: true }); return RichUtils.toggleBlockType(EditorState.push(editorState, Modifier.removeRange(contentState, allSelected, 'backward'), 'remove-range'), 'unstyled'); }; export const handleKeyCommand = (editorState, command) => { return RichUtils.handleKeyCommand(editorState, command); }; export const undo = (editorState) => { return EditorState.undo(editorState); }; export const redo = (editorState) => { return EditorState.redo(editorState); }; let uniqueIndex = 0; export const UniqueIndex = () => { uniqueIndex += 1; return uniqueIndex; }; export const getHexColor = (color) => { color = color.replace('color:', '').replace(';', '').replace(' ', ''); if (/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(color)) { return color; } else if (namedColors[color]) { return namedColors[color]; } else if (color.indexOf('rgb') === 0) { const rgbArray = color.split(','); const convertedColor = rgbArray.length < 3 ? null : '#' + [rgbArray[0], rgbArray[1], rgbArray[2]] .map((x) => { const hex = parseInt(x.replace(/\D/g, ''), 10).toString(16); return hex.length === 1 ? '0' + hex : hex; }) .join(''); return /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(convertedColor) ? convertedColor : null; } else { return null; } }; export const detectColorsFromHTMLString = (html) => { return typeof html !== 'string' ? [] : (html.match(/color:[^;]{3,24};/g) || []) .map(getHexColor) .filter((color) => color); }; export const detectColorsFromDraftState = (draftState) => { const result = []; if (!draftState || !draftState.blocks || !draftState.blocks.length) { return result; } draftState.blocks.forEach((block) => { var _a; if ((_a = block === null || block === void 0 ? void 0 : block.inlineStyleRanges) === null || _a === void 0 ? void 0 : _a.length) { block.inlineStyleRanges.forEach((inlineStyle) => { var _a; if ((_a = inlineStyle.style) === null || _a === void 0 ? void 0 : _a.includes('COLOR-')) { result.push('#' + inlineStyle.style.split('COLOR-')[1]); } }); } }); return result.filter((color) => color); }; /** * Function will handle followind keyPress scenario: * case Shift+Enter, select not collapsed -> * selected text will be removed and line break will be inserted. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/block.js */ export function addLineBreakRemovingSelection(editorState) { const content = editorState.getCurrentContent(); const selection = editorState.getSelection(); let newContent = Modifier.removeRange(content, selection, 'forward'); const fragment = newContent.getSelectionAfter(); const block = newContent.getBlockForKey(fragment.getStartKey()); newContent = Modifier.insertText(newContent, fragment, '\n', block.getInlineStyleAt(fragment.getStartOffset()), null); return EditorState.push(editorState, newContent, 'insert-fragment'); } /** * This function will return the entity applicable to whole of current selection. * An entity can not span multiple blocks. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/inline.js */ export const getSelectionEntity = (editorState) => { let entity; const selection = editorState.getSelection(); let start = selection.getStartOffset(); let end = selection.getEndOffset(); if (start === end && start === 0) { end = 1; } else if (start === end) { start -= 1; } const block = getSelectedBlock(editorState); for (let i = start; i < end; i += 1) { const currentEntity = block.getEntityAt(i); if (!currentEntity) { entity = undefined; break; } if (i === start) { entity = currentEntity; } else if (entity !== currentEntity) { entity = undefined; break; } } return entity; }; /** * Function will add block level meta-data. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/block.js */ export const setBlockData = (editorState, data) => { const newContentState = Modifier.setBlockData(editorState.getCurrentContent(), editorState.getSelection(), data); return EditorState.push(editorState, newContentState, 'change-block-data'); }; /** * The function will handle keypress 'Enter' in editor. Following are the scenarios: * * 1. Shift+Enter, Selection Collapsed -> line break will be inserted. * 2. Shift+Enter, Selection not Collapsed -> * selected text will be removed and line break will be inserted. * 3. Enter, Selection Collapsed -> * if current block is of type list and length of block is 0 * a new list block of depth less that current one will be inserted. * 4. Enter, Selection Collapsed -> * if current block not of type list, a new unstyled block will be inserted. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/keyPress.js */ export const handleNewLine = (editorState, event) => { if (KeyBindingUtil.isSoftNewlineEvent(event)) { const selection = editorState.getSelection(); if (selection.isCollapsed()) { return RichUtils.insertSoftNewline(editorState); } return addLineBreakRemovingSelection(editorState); } return handleHardNewlineEvent(editorState); }; /** * Function to check if a block is of type list. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/list.js */ export function isListBlock(block) { if (block) { const blockType = block.getType(); return (blockType === 'unordered-list-item' || blockType === 'ordered-list-item'); } return false; } /** * Function to change depth of block(s). * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/list.js */ const changeBlocksDepth = (editorState, adjustment, maxDepth) => { const selectionState = editorState.getSelection(); const contentState = editorState.getCurrentContent(); let blockMap = contentState.getBlockMap(); const blocks = getSelectedBlocks(editorState).map((block) => { let depth = block.getDepth() + adjustment; depth = Math.max(0, Math.min(depth, maxDepth)); return block.set('depth', depth); }); blockMap = blockMap.merge(...blocks); return contentState.merge({ blockMap, selectionBefore: selectionState, selectionAfter: selectionState }); }; /** * Function will check various conditions for changing depth and will accordingly * either call function changeBlocksDepth or just return the call. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/list.js */ const changeDepth = (editorState, adjustment, maxDepth) => { const selection = editorState.getSelection(); let key; if (selection.getIsBackward()) { key = selection.getFocusKey(); } else { key = selection.getAnchorKey(); } const content = editorState.getCurrentContent(); const block = content.getBlockForKey(key); const type = block.getType(); if (type !== 'unordered-list-item' && type !== 'ordered-list-item') { return editorState; } const blockAbove = content.getBlockBefore(key); if (!blockAbove) { return editorState; } const typeAbove = blockAbove.getType(); if (typeAbove !== type) { return editorState; } const depth = block.getDepth(); if (adjustment === 1 && depth === maxDepth) { return editorState; } const adjustedMaxDepth = Math.min(blockAbove.getDepth() + 1, maxDepth); const withAdjustment = changeBlocksDepth(editorState, adjustment, adjustedMaxDepth); return EditorState.push(editorState, withAdjustment, 'adjust-depth'); }; /** * Function will handle followind keyPress scenarios when Shift key is not pressed. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/keyPress.js */ const handleHardNewlineEvent = (editorState) => { const selection = editorState.getSelection(); if (selection.isCollapsed()) { const contentState = editorState.getCurrentContent(); const blockKey = selection.getStartKey(); const block = contentState.getBlockForKey(blockKey); if (!isListBlock(block) && block.getType() !== 'unstyled' && block.getLength() === selection.getStartOffset()) { return insertNewUnstyledBlock(editorState); } if (isListBlock(block) && block.getLength() === 0) { const depth = block.getDepth(); if (depth === 0) { return removeSelectedBlocksStyle(editorState); } if (depth > 0) { return changeDepth(editorState, -1, depth); } } } return undefined; }; /** * Function will inset a new unstyled block. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/block.js */ const insertNewUnstyledBlock = (editorState) => { const newContentState = Modifier.splitBlock(editorState.getCurrentContent(), editorState.getSelection()); const newEditorState = EditorState.push(editorState, newContentState, 'split-block'); return removeSelectedBlocksStyle(newEditorState); }; /** * Function will change block style to unstyled for selected blocks. * RichUtils.tryToRemoveBlockStyle does not workd for blocks of length greater than 1. * Credit: https://github.com/jpuri/draftjs-utils/blob/9e96939aa4a41bb89ad57f8c71c6a8c27efb76f8/js/block.js */ const removeSelectedBlocksStyle = (editorState) => { const newContentState = RichUtils.tryToRemoveBlockStyle(editorState); if (newContentState) { return EditorState.push(editorState, newContentState, 'change-block-type'); } return editorState; }; export const compressImage = (url, width = 1280, height = 800) => __awaiter(void 0, void 0, void 0, function* () { return yield new Promise((resolve, reject) => { const image = new Image(); image.src = url; image.onerror = function (error) { reject(error); }; image.onload = function () { try { const compressCanvas = document.createElement('canvas'); const scale = this.width > width || this.height > height ? this.width > this.height ? width / this.width : height / this.height : 1; compressCanvas.width = this.width * scale; compressCanvas.height = this.height * scale; const canvasContext = compressCanvas.getContext('2d'); canvasContext.drawImage(this, 0, 0, compressCanvas.width, compressCanvas.height); resolve({ url: compressCanvas.toDataURL('image/png', 1), width: compressCanvas.width, height: compressCanvas.height }); } catch (error) { reject(error); } }; }); }); const getStyleValue = (style) => style.split('-')[1]; const defaultUnitExportFn = (unit) => unit + 'px'; const defaultUnitImportFn = (unit) => unit.replace('px', ''); const ignoredNodeAttributes = ['style']; const ignoredEntityNodeAttributes = [ 'style', 'href', 'target', 'alt', 'title', 'id', 'controls', 'autoplay', 'loop', 'poster' ]; const spreadNodeAttributes = (attributesObject) => { return Object.keys(attributesObject) .reduce((attributeString, attributeName) => { return `${attributeString} ${attributeName}="${attributesObject[attributeName]}"`; }, '') .replace(/^\s$/, ''); }; const blocks = { 'header-one': 'h1', 'header-two': 'h2', 'header-three': 'h3', 'header-four': 'h4', 'header-five': 'h5', 'header-six': 'h6', unstyled: 'p', blockquote: 'blockquote' }; const blockTypes = Object.keys(blocks); const blockNames = blockTypes.map((key) => blocks[key]); const convertAtomicBlock = (block, contentState, blockNodeAttributes) => { if (!block || !block.key) { return React.createElement("p", null); } const contentBlock = contentState.getBlockForKey(block.key); const { class: className } = blockNodeAttributes, nodeAttrAsProps = __rest(blockNodeAttributes, ["class"]); nodeAttrAsProps.className = className; if (!contentBlock) { return React.createElement("p", null); } const entityKey = contentBlock.getEntityAt(0); if (!entityKey) { return React.createElement("p", null); } const entity = contentState.getEntity(entityKey); const mediaType = entity.getType().toLowerCase(); const { float, alignment } = block.data; // eslint-disable-next-line @typescript-eslint/naming-convention const { url, link, link_target, width, height, meta } = entity.getData(); if (mediaType === 'image') { const imageWrapStyle = {}; let styledClassName = ''; if (float) { imageWrapStyle.float = float; styledClassName += ' kedao-float-' + float; } else if (alignment) { imageWrapStyle.textAlign = alignment; styledClassName += ' kedao-align-' + alignment; } if (link) { return (React.createElement("div", { className: styledClassName, style: imageWrapStyle }, React.createElement("a", { style: { display: 'inline-block' }, href: link, target: link_target }, React.createElement("img", Object.assign({}, nodeAttrAsProps, meta, { src: url, width: width, height: height, style: { width, height } }))))); } else { return (React.createElement("div", { className: styledClassName, style: imageWrapStyle }, React.createElement("img", Object.assign({}, nodeAttrAsProps, meta, { src: url, width: width, height: height, style: { width, height } })))); } } else if (mediaType === 'audio') { return (React.createElement("div", null, React.createElement("audio", Object.assign({ controls: true }, nodeAttrAsProps, meta, { src: url })))); } else if (mediaType === 'video') { return (React.createElement("div", null, React.createElement("video", Object.assign({ controls: true }, nodeAttrAsProps, meta, { src: url, width: width, height: height })))); } else if (mediaType === 'embed') { return (React.createElement("div", null, React.createElement("div", { dangerouslySetInnerHTML: { __html: url } }))); } else if (mediaType === 'hr') { return React.createElement("hr", null); } else { return React.createElement("p", null); } }; const entityToHTML = (options) => (entity, originalText) => { var _a; const { entityExportFn } = options; const entityType = entity.type.toLowerCase(); if (entityExportFn) { const customOutput = entityExportFn(entity, originalText); if (customOutput) { return customOutput; } } if (entityType === 'link') { const _b = (_a = entity.data.nodeAttributes) !== null && _a !== void 0 ? _a : {}, { class: className } = _b, nodeAttrAsProps = __rest(_b, ["class"]); nodeAttrAsProps.className = className; return (React.createElement("a", Object.assign({ href: entity.data.href, target: entity.data.target }, nodeAttrAsProps))); } }; const styleToHTML = (options) => (style) => { var _a; const unitExportFn = (_a = options.unitExportFn) !== null && _a !== void 0 ? _a : defaultUnitExportFn; if (options.styleExportFn) { const customOutput = options.styleExportFn(style, options); if (customOutput) { return customOutput; } } style = style.toLowerCase(); if (style === 'strikethrough') { return React.createElement("span", { style: { textDecoration: 'line-through' } }); } else if (style === 'superscript') { return React.createElement("sup", null); } else if (style === 'subscript') { return React.createElement("sub", null); } else if (style.indexOf('color-') === 0) { return React.createElement("span", { style: { color: '#' + getStyleValue(style) } }); } else if (style.indexOf('bgcolor-') === 0) { return React.createElement("span", { style: { backgroundColor: '#' + getStyleValue(style) } }); } else if (style.indexOf('fontsize-') === 0) { return (React.createElement("span", { style: { fontSize: unitExportFn(getStyleValue(style), 'font-size', 'html') } })); } else if (style.indexOf('lineheight-') === 0) { return (React.createElement("span", { style: { lineHeight: unitExportFn(getStyleValue(style), 'line-height', 'html') } })); } else if (style.indexOf('letterspacing-') === 0) { return (React.createElement("span", { style: { letterSpacing: unitExportFn(getStyleValue(style), 'letter-spacing', 'html') } })); } else if (style.indexOf('fontfamily-') === 0) { const fontFamily = options.fontFamilies.find((item) => item.name.toLowerCase() === getStyleValue(style)); if (!fontFamily) return; return React.createElement("span", { style: { fontFamily: fontFamily.family } }); } }; const blockToHTML = (options) => (block) => { const { blockExportFn, contentState } = options; if (blockExportFn) { const customOutput = blockExportFn(contentState, block); if (customOutput) { return customOutput; } } let blockStyle = ''; const blockType = block.type.toLowerCase(); const { textAlign, textIndent, nodeAttributes = {} } = block.data; const attributeString = spreadNodeAttributes(nodeAttributes); if (textAlign || textIndent) { blockStyle = ' style="'; if (textAlign) { blockStyle += `text-align:${textAlign};`; } if (textIndent && !isNaN(textIndent) && textIndent > 0) { blockStyle += `text-indent:${textIndent * 2}em;`; } blockStyle += '"'; } if (blockType === 'atomic') { return convertAtomicBlock(block, contentState, nodeAttributes); } else if (blockType === 'code-block') { const previousBlock = contentState.getBlockBefore(block.key); const nextBlock = contentState.getBlockAfter(block.key); const previousBlockType = previousBlock === null || previousBlock === void 0 ? void 0 : previousBlock.getType(); const nextBlockType = nextBlock === null || nextBlock === void 0 ? void 0 : nextBlock.getType(); let start = ''; let end = ''; if (previousBlockType !== 'code-block') { start = `<pre${attributeString}><code>`; } else { start = ''; } if (nextBlockType !== 'code-block') { end = '</code></pre>'; } else { end = '<br/>'; } return { start, end }; } else if (blocks[blockType]) { return { start: `<${blocks[blockType]}${blockStyle}${attributeString}>`, end: `</${blocks[blockType]}>` }; } else if (blockType === 'unordered-list-item') { return { start: `<li${blockStyle}${attributeString}>`, end: '</li>', nest: React.createElement("ul", null) }; } else if (blockType === 'ordered-list-item') { return { start: `<li${blockStyle}${attributeString}>`, end: '</li>', nest: React.createElement("ol", null) }; } }; const htmlToStyle = (options, source) => (nodeName, node, currentStyle) => { if (!node || !node.style) { return currentStyle; } const unitImportFn = options.unitImportFn || defaultUnitImportFn; let newStyle = currentStyle; [].forEach.call(node.style, (style) => { if (nodeName === 'span' && style === 'color') { const color = getHexColor(node.style.color); newStyle = color ? newStyle.add('COLOR-' + color.replace('#', '').toUpperCase()) : newStyle; } else if (nodeName === 'span' && style === 'background-color') { const color = getHexColor(node.style.backgroundColor); newStyle = color ? newStyle.add('BGCOLOR-' + color.replace('#', '').toUpperCase()) : newStyle; } else if (nodeName === 'span' && style === 'font-size') { newStyle = newStyle.add('FONTSIZE-' + unitImportFn(node.style.fontSize, 'font-size', source)); } else if (nodeName === 'span' && style === 'line-height' && !isNaN(parseFloat(node.style.lineHeight))) { newStyle = newStyle.add('LINEHEIGHT-' + unitImportFn(node.style.lineHeight, 'line-height', source)); } else if (nodeName === 'span' && style === 'letter-spacing' && !isNaN(parseFloat(node.style.letterSpacing))) { newStyle = newStyle.add('LETTERSPACING-' + unitImportFn(node.style.letterSpacing, 'letter-spacing', source)); } else if (nodeName === 'span' && style === 'text-decoration') { if (node.style.textDecoration === 'line-through') { newStyle = newStyle.add('STRIKETHROUGH'); } else if (node.style.textDecoration === 'underline') { newStyle = newStyle.add('UNDERLINE'); } } else if (nodeName === 'span' && style === 'font-family') { const fontFamily = options.fontFamilies.find((item) => item.family.toLowerCase() === node.style.fontFamily.toLowerCase()); if (!fontFamily) return; newStyle = newStyle.add('FONTFAMILY-' + fontFamily.name.toUpperCase()); } }); if (nodeName === 'sup') { newStyle = newStyle.add('SUPERSCRIPT'); } else if (nodeName === 'sub') { newStyle = newStyle.add('SUBSCRIPT'); } options.styleImportFn && (newStyle = options.styleImportFn(nodeName, node, newStyle, source) || newStyle); return newStyle; }; const htmlToEntity = (options, source) => (nodeName, node, createEntity) => { var _a; if (options === null || options === void 0 ? void 0 : options.entityImportFn) { const customInput = options.entityImportFn(nodeName, node, createEntity, source); if (customInput) { return customInput; } } nodeName = nodeName.toLowerCase(); const { alt, title, id, controls, autoplay, loop, poster } = node; const meta = {}; const nodeAttributes = {}; id && (meta.id = id); alt && (meta.alt = alt); title && (meta.title = title); controls && (meta.controls = controls); autoplay && (meta.autoPlay = autoplay); loop && (meta.loop = loop); poster && (meta.poster = poster); node.attributes && Object.keys(node.attributes).forEach((key) => { const attr = node.attributes[key]; !ignoredEntityNodeAttributes.includes(attr.name) && (nodeAttributes[attr.name] = attr.value); }); if (nodeName === 'a' && node.querySelectorAll('img').length > 0) { const href = node.getAttribute('href'); const target = node.getAttribute('target'); return createEntity('LINK', 'MUTABLE', { href, target, nodeAttributes }); } else if (nodeName === 'audio') { return createEntity('AUDIO', 'IMMUTABLE', { url: node.getAttribute('src'), meta, nodeAttributes }); } else if (nodeName === 'video') { return createEntity('VIDEO', 'IMMUTABLE', { url: node.getAttribute('src'), meta, nodeAttributes }); } else if (nodeName === 'img') { const parentNode = node.parentNode; const entityData = { meta }; const { width, height } = node.style; entityData.url = node.getAttribute('src'); width && (entityData.width = width); height && (entityData.height = height); if (parentNode.nodeName.toLowerCase() === 'a') { entityData.link = parentNode.getAttribute('href'); entityData.link_target = parentNode.getAttribute('target'); } return createEntity('IMAGE', 'IMMUTABLE', entityData); } else if (nodeName === 'hr') { return createEntity('HR', 'IMMUTABLE', {}); } else if ((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.classList.contains('embed-wrap')) { const embedContent = node.innerHTML || node.outerHTML; if (embedContent) { return createEntity('EMBED', 'IMMUTABLE', { url: embedContent }); } } }; const htmlToBlock = (options, source) => (nodeName, node) => { var _a, _b; if (options === null || options === void 0 ? void 0 : options.blockImportFn) { const customInput = options.blockImportFn(nodeName, node, source); if (customInput) { return customInput; } } const nodeAttributes = {}; const nodeStyle = (_a = node.style) !== null && _a !== void 0 ? _a : {}; node.attributes && Object.keys(node.attributes).forEach((key) => { const attr = node.attributes[key]; !ignoredNodeAttributes.includes(attr.name) && (nodeAttributes[attr.name] = attr.value); }); if ((_b = node.classList) === null || _b === void 0 ? void 0 : _b.contains('media-wrap')) { return { type: 'atomic', data: { nodeAttributes: nodeAttributes, float: nodeStyle.float, alignment: nodeStyle.textAlign } }; } else if (nodeName === 'img') { return { type: 'atomic', data: { nodeAttributes: nodeAttributes, float: nodeStyle.float, alignment: nodeStyle.textAlign } }; } else if (nodeName === 'hr') { return { type: 'atomic', data: { nodeAttributes } }; } else if (nodeName === 'pre') { node.innerHTML = node.innerHTML .replace(/<code(.*?)>/g, '') .replace(/<\/code>/g, ''); return { type: 'code-block', data: { nodeAttributes } }; } else if (blockNames.includes(nodeName)) { const blockData = { nodeAttributes }; if (nodeStyle.textAlign) { blockData.textAlign = nodeStyle.textAlign; } if (nodeStyle.textIndent) { blockData.textIndent = /^\d+em$/.test(nodeStyle.textIndent) ? Math.ceil(parseInt(nodeStyle.textIndent, 10) / 2) : 1; } return { type: blockTypes[blockNames.indexOf(nodeName)], data: blockData }; } }; export const getToHTMLConfig = (options) => { return { styleToHTML: styleToHTML(options), entityToHTML: entityToHTML(options), blockToHTML: blockToHTML(options) }; }; export const getFromHTMLConfig = (options, source = 'unknow') => { return { htmlToStyle: htmlToStyle(options, source), htmlToEntity: htmlToEntity(options, source), htmlToBlock: htmlToBlock(options, source) }; }; const defaultConvertOptions = { fontFamilies: defaultFontFamilies }; export const convertRawToHTML = (rawContent, options) => { options = Object.assign(Object.assign({}, defaultConvertOptions), options); try { const contentState = convertFromRaw(rawContent); options.contentState = contentState; return convertToHTML(getToHTMLConfig(options))(contentState); } catch (error) { console.warn(error); return ''; } }; export const convertHTMLToRaw = (HTMLString, options, source) => { options = Object.assign(Object.assign({}, defaultConvertOptions), options); try { const contentState = convertFromHTML(getFromHTMLConfig(options, source))(HTMLString); return convertToRaw(contentState); } catch (error) { console.warn(error); return {}; } }; export const convertEditorStateToHTML = (editorState, options = {}) => { options = Object.assign(Object.assign({}, defaultConvertOptions), options); try { const contentState = editorState.getCurrentContent(); options.contentState = contentState; return convertToHTML(getToHTMLConfig(options))(contentState); } catch (error) { console.warn(error); return ''; } }; export const convertHTMLToEditorState = (HTMLString, editorDecorators, options, source) => { options = Object.assign(Object.assign({}, defaultConvertOptions), options); try { return EditorState.createWithContent(convertFromHTML(getFromHTMLConfig(options, source))(HTMLString), editorDecorators); } catch (error) { console.warn(error); return EditorState.createEmpty(editorDecorators); } }; export const convertEditorStateToRaw = (editorState) => { return convertToRaw(editorState.getCurrentContent()); }; export const convertRawToEditorState = (rawContent, editorDecorators) => { try { return EditorState.createWithContent(convertFromRaw(rawContent), editorDecorators); } catch (error) { console.warn(error); return EditorState.createEmpty(editorDecorators); } }; export { convertFromRaw, convertToRaw };