UNPKG

@kedao/utils

Version:

Utils for Kedao Editor

710 lines (603 loc) 18.2 kB
import { Modifier, EditorState, SelectionState, RichUtils, CharacterMetadata, AtomicBlockUtils, convertFromRaw } from 'draft-js' import { setBlockData, getSelectionEntity } from 'draftjs-utils' import { convertHTMLToRaw } from '@kedao/convert' import Immutable from 'immutable' const strictBlockTypes = ['atomic'] export const registerStrictBlockType = (blockType) => { !strictBlockTypes.includes(blockType) && strictBlockTypes.push(blockType) } export const isEditorState = (editorState) => { return editorState instanceof EditorState } export const createEmptyEditorState = (editorDecorators) => { return EditorState.createEmpty(editorDecorators) } export const createEditorState = (contentState, editorDecorators) => { return EditorState.createWithContent(contentState, editorDecorators) } export const isSelectionCollapsed = (editorState) => { return editorState.getSelection().isCollapsed() } 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, 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 }) } else if (blockKey === startKey) { nextCharactersList = charactersList.map((character, index) => { if (index >= startOffset) { return callback(character) } return character }) } else if (blockKey === endKey) { nextCharactersList = charactersList.map((character, index) => { if (index < endOffset) { return callback(character) } return character }) } else { nextCharactersList = charactersList.map((character) => { return callback(character) }) } return block.merge({ characterList: nextCharactersList }) }) return EditorState.push( editorState, contentState.merge({ blockMap: nextContentBlocks, selectionBefore: selectionState, selectionAfter: selectionState }), 'update-selection-character-list' as any ) } 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) 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?: string) => { 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.get('type') : null } return null } export const getSelectionEntityData = (editorState, type) => { const entityKey = getSelectionEntity(editorState) if (entityKey) { const entity = editorState.getCurrentContent().getEntity(entityKey) if (entity && entity.get('type') === 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' as any ) 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: string, 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: number = 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?.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' as any ) } else { return EditorState.push( editorState, Modifier.insertText( contentState, selectionState, text, inlineStyle, entityKey ), 'insert-text' as any ) } } export const insertHTML = (editorState, htmlString, source) => { if (!htmlString) { return editorState } const selectionState = editorState.getSelection() const contentState = editorState.getCurrentContent() const options = editorState.convertOptions || {} try { const { blockMap } = convertFromRaw( convertHTMLToRaw(htmlString, options, source) as any ) as any 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, mediaBlock, position) => { const newPosition: Record<string, any> = {} 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) }