UNPKG

@unicef/material-slate

Version:

Material UI rich text editor based on Slate for react

281 lines (260 loc) 8.79 kB
import MaterialEditor from '../slate/MaterialEditor' import { Range } from 'slate' import { Transforms } from 'slate' import { Node } from 'slate' import { ReactEditor } from 'slate-react' /** * * Base plugin for Material Slate. * * All other plugins assume this plugin exists and has been included. * * @param {Editor} editor */ const withBase = editor => { /** * Is the current editor selection a range, that is the focus and the anchor are different? * * @returns {boolean} true if the current selection is a range. */ editor.isSelectionExpanded = () => { return editor.selection ? Range.isExpanded(editor.selection) : false } /** * Returns true if current selection is collapsed, that is there is no selection at all * (the focus and the anchor are the same). * * @returns {boolean} true if the selection is collapsed */ editor.isSelectionCollapsed = () => { return !editor.isSelectionExpanded() } /** * Is the editor focused? * @returns {boolean} true if the editor has focus. */ editor.isFocused = () => { return ReactEditor.isFocused(editor) } /** * Unwraps any node of `type` within the current selection. */ editor.unwrapNode = type => { Transforms.unwrapNodes(editor, { match: n => n.type === type }) } /** * * @param {string} type type of node to be checked. Example: `comment`, `numbered-list` * * @returns {bool} true if within current selection there is a node of type `type` */ editor.isNodeTypeActive = type => { const [node] = MaterialEditor.nodes(editor, { match: n => n.type === type }) return !!node } /** * Variable for holding a selection may be forgotten. */ editor.rememberedSelection = {} /** * Gets current selection and stores it in rememberedSelection. * * This may be useful when you need to open a dialog box and the editor loses the focus */ editor.rememberCurrentSelection = () => { editor.rememberedSelection = editor.selection } /** * Is the current selection collapsed? */ editor.isCollapsed = () => { const { selection } = editor //console.log('selection', selection) return selection && Range.isCollapsed(selection) } /** * Wraps a selection with an argument. If `wrapSelection` is not passed * uses current selection * * Upon wrapping moves the cursor to the end. * * @param {Node} node the node to be added * @param {Selection} wrapSelection selection of the text that will be wrapped with the node. * */ editor.wrapNode = (node, wrapSelection = null) => { //if wrapSelection is passed => we use it. Use editor selection in other case editor.selection = wrapSelection ? wrapSelection : editor.selection // if the node is already wrapped with current node we unwrap it first. if (editor.isNodeTypeActive(node.type)) { editor.unwrapNode(node.type) } // if there is no text selected => insert the node. //console.log('isLocation', Location.isLocation(editor.selection)) if (editor.isCollapsed()) { //console.log('is collapsed insertNodes') Transforms.insertNodes(editor, node) } else { //text is selected => add the node Transforms.wrapNodes(editor, node, { split: true }) //console.log('editor', editor.children) Transforms.collapse(editor, { edge: 'end' }) } // Add {isLast} property to the last fragment of the comment. const path = [...MaterialEditor.last(editor, editor.selection)[1]] //The last Node is a text whose parent is a comment. path.pop() // Removes last item of the path, to point the parent Transforms.setNodes(editor, { isLast: true }, { at: path }) //add isLast } /** * Unwraps or removes the nodes that are not in the list. * * It will search for all the nodes of `type` in the editor and will keep only * the ones in the nodesToKeep. * * It assumes each item of nodesToKeep has an attribute `id`. This attribute will be the discriminator. * */ editor.syncExternalNodes = (type, nodesToKeep, unwrap = true) => { //extracts the id from the nodes and removes those that are not in the list const listOfIds = nodesToKeep.map(node => node.id) if (unwrap) { editor.unwrapNotInList(type, listOfIds) } else { editor.removeNotInList(type, listOfIds) } const nodesToKeepObj = {} //Update values of nodes.data //Create a map by id of the nodes to keep nodesToKeep.forEach(node => (nodesToKeepObj[node.id] = node)) //Find nodes of this type remaining in the editor const editorNodes = editor.findNodesByType(type) //Update them editorNodes.map(node => { Transforms.setNodes( editor, { data: nodesToKeepObj[node.id] }, { match: n => n.id == node.id, at: [] } ) }) } /** * Removes the nodes that are not in the list of Ids * * Nodes of type `type` shall have the attribute/property `id` * * Example: * ``` * { * type: `comment` * id: 30 * data: { ... } * } * ``` */ editor.removeNotInList = (type, listOfIds) => { Transforms.removeNodes(editor, { match: n => n.type === type && !listOfIds.includes(n.id), at: [], //Search the whole editor content }) } /** * * Unwraps the nodes of `type` whose ids are not in the provided list * * It assumes the nodes of `type` have an attribute `id`. The `id` may be a number or string. * * @param {string} type node type to be searched. Example: `comment` * @param {Array} listOfIds Array with the list of ids. Example: [1, 2, 3]. */ editor.unwrapNotInList = (type, listOfIds) => { Transforms.unwrapNodes(editor, { match: n => n.type === type && !listOfIds.includes(n.id), at: [], //Search the whole editor content }) } /** * Gets from current editor content the list of items of a particular type */ editor.findNodesByType = type => { const list = MaterialEditor.nodes(editor, { match: n => n.type === type, at: [], }) // List in editor with path and node const listWithNodesAndPath = Array.from(list) // List with node (element) const listWithNodes = listWithNodesAndPath.map(item => { return item[0] }) //console.log('fondNodesByType ', listWithNodes) return listWithNodes } /** * Returns the serialized value (plain text) */ editor.serialize = nodes => { return nodes.map(n => Node.string(n)).join('\n') } /** * Functions similar to syncExternalNodes,and also updates the node temporaryId with original id and data * * First, It will search for match in temporaryId's in nodesToKeep with id's of nodes and updates it with latest data * Then, updates data in node id's matching with nodesToKeep id's * * Unwraps or removes the nodes that are not in the list. */ editor.syncExternalNodesWithTemporaryId = ( type, nodesToKeep, unwrap = true ) => { //extracts the id from the nodes and removes those that are not in the list const listOfIds = nodesToKeep.map(node => node.id) const nodesToKeepObj = {} //Update values of nodes.data //Create a map by id of the nodes to keep nodesToKeep.forEach(node => (nodesToKeepObj[node.id] = node)) //Find nodes of this type remaining in the editor const editorNodes = editor.findNodesByType(type) //Update them editorNodes.map(node => { // Find the key of node to update const key = Object.keys(nodesToKeepObj).find( key => nodesToKeepObj[key].temporaryId === node.id ) // node to Update with original Id and data const nodeToUpdate = nodesToKeepObj[key] // If node.id exists if (nodesToKeepObj[node.id] && !nodeToUpdate) { Transforms.setNodes( editor, { data: nodesToKeepObj[node.id] }, { match: n => n.id == node.id, at: [0] } ) // TemporaryId and data will be replaced with new id and data } else if (key && nodeToUpdate) { Transforms.setNodes( editor, { id: nodeToUpdate.id, data: nodeToUpdate }, { match: n => n.id == nodeToUpdate.temporaryId, at: [] } ) } else if (unwrap) { // unwraps the nodes in not list editor.unwrapNotInList(type, listOfIds) } else { // removes the nodes in not list editor.removeNotInList(type, listOfIds) } }) } /** * Is to get the selected plain text from the editor.selection * * @returns {string} selected text */ editor.getSelectedText = () => { return MaterialEditor.string(editor, editor.rememberedSelection) } return editor } export default withBase