import PropTypes from 'prop-types'; import {Map, OrderedSet, List, Repeat} from 'immutable'; import { EditorState, ContentState, ContentBlock, RichUtils, SelectionState, convertToRaw, CharacterMetadata, genKey, } from 'draft-js'; import insertAtomicBlockWithoutEmptyLines from '../../helpers/insertAtomicBlockWithoutEmptyLines'; /** * @name mockStore * @description Creates a new Redux store mock and returns an object containing * two keys: "store" as the actual store, and "options" as the parameter to be * passed to enzyme's mount command in order to use this store as context. * @param {Object} state The state the store should have when queried. * @returns {Object} The object containing the store and the enzyme mount param. */ export default function mockStore(state = {}) { const store = { subscribe: () => ({}), dispatch: () => ({}), getState: () => state, }; const options = { context: {store}, childContextTypes: { store: PropTypes.object.isRequired, }, }; return {store, options}; } /** * @name stateWithLink * @description Returns a new editorState that has the text "click HERE to open page" * as content, having the uppercased letters marked as an entity of type LINK, containing * the data {link: {href: 'entity-url'}} * @returns {Object} editorState */ export function stateWithLink() { const contentState = ContentState .createFromText('click HERE to open page') .createEntity('LINK', 'MUTABLE', {link: {href: 'entity-url'}}); const entityKey = contentState.getLastCreatedEntityKey(); const blockKey = contentState.getFirstBlock().getKey(); const linkSelection = SelectionState.createEmpty(blockKey).merge({ anchorOffset: 6, focusOffset: 9, }); return RichUtils.toggleLink( EditorState.createWithContent(contentState), linkSelection as SelectionState, entityKey, ); } /** * @description Returns a block containing an image and it's host content state. * @returns {Object} Keys 'block' and 'contentState' */ export function imageBlockAndContent() { return createBlockAndContent('MEDIA', { media: { _type: 'archive', // make sure edit icon is visible renditions: {original: {href: 'image_href'}}, alt_text: 'image_alt_text', headline: 'image_headline', description_text: 'image_description', type: 'picture', }, }); } /** * @description Creates a contentState containing an atomic block with the entity * having the given type and containing the given data. * @param {String} type Entity type * @param {Object} data Entity data * @returns {Object} Keys 'block' and 'contentState' */ export function createBlockAndContent(type, data) { const cs = ContentState.createFromText('here is an image:'); const contentState = cs.createEntity(type, 'MUTABLE', data); const entityKey = contentState.getLastCreatedEntityKey(); const {editorState} = insertAtomicBlockWithoutEmptyLines( EditorState.createWithContent(contentState), entityKey, ' '); const block = editorState.getCurrentContent().getBlocksAsArray()[1]; return {block: block, contentState: editorState.getCurrentContent()}; } /** * @description Creates a contentState containing an atomic block that is an embed. * @returns {Object} Keys 'block' and 'contentState' */ export function embedBlockAndContent() { return createBlockAndContent('EMBED', { data: { html: '

Embed Title

', }, }); } /** * @description Creates a new content state containing a given list of regular blocks. * @param {Array} Contains an array that describes blocks. Each item is an array * of 3 elements, describing block properties: style, depth and text. * @returns {ContentState} */ export function blocksWithText(list) { return ContentState.createFromBlockArray(list.map((htmlData) => { const type = htmlData[0] || 'unstyled'; const depth = htmlData[1] || 0; const text = htmlData[2] || ''; const data = htmlData[3] || {}; return new ContentBlock({ type: type, key: genKey(), depth: depth, data: Map(data), text: text, characterList: List(Repeat(CharacterMetadata.create({ style: OrderedSet([]), entity: null, }), text.length)), }); })); } /** * @description Creates a contentState containing an atomic block that is a table * containing the given cells. If omitted, a default table is returned, containing * two rows and three columns. * @param {Array=} cells * @returns {Object} Keys 'block' and 'contentState' */ export function tableBlockAndContent(cells?) { const cs = (txt) => convertToRaw(ContentState.createFromText(txt)); return createBlockAndContent('TABLE', { data: { numCols: 3, numRows: 2, cells: cells || [ [cs('a'), cs('b'), cs('c')], [cs('d'), cs('e'), cs('f')], ], }, }); } export function cursorAtEndPosition(editorState): EditorState { const content = editorState.getCurrentContent(); const blockMap = content.getBlockMap(); const key = blockMap.last().getKey(); const length = blockMap.last().getLength(); const selection = new SelectionState({ anchorKey: key, anchorOffset: length, focusKey: key, focusOffset: length, }); return EditorState.forceSelection(editorState, selection); } /** * @name cursorAtPosition * @description Takes an editorState and returns it with the selection set to position * pos. If 'n' is supplied, pos + n will be the focus of the selection. * @param {Number} pos The anchor point of the selection * @param {Number=} n The offset for the focus point of the selection (pos+n) * @returns {Object} editorState */ export function cursorAtPosition(editorState, pos, n = 0) { const blockKey = editorState.getCurrentContent() .getFirstBlock() .getKey(); const selection = SelectionState.createEmpty(blockKey).merge({ anchorOffset: pos, focusOffset: pos + n, }) as SelectionState; return EditorState.forceSelection(editorState, selection); }