Search code examples
javascriptreactjsdraftjsdraft-js-plugins

Draftjs replaceText conserving style


I'm using Draftjs with draft-js-plugins-editor, I'm using two plugins : draft-js-mathjax-plugin and draft-js-mention-plugin

When the user is mentioning element(s) using '@' I want later to replace all the mentions by values. For example "You have @A" will be replace by "You have 300". I found and used the Draft-js building search and replace functionality which is well documented and explained. I changed a bit the function to make them more global:

function findWithRegex (regex, contentBlock, callback) {
    const text = contentBlock.getText();
    let matchArr, start, end;
    while ((matchArr = regex.exec(text)) !== null) {
        start = matchArr.index;
        end = start + matchArr[0].length;
        callback(start, end);
    }
}

function Replace (editorState,search,replace) {
    const regex = new RegExp(search, 'g');
    const selectionsToReplace = [];
    const blockMap = editorState.getCurrentContent().getBlockMap();

    blockMap.forEach((contentBlock,i) => (
        findWithRegex(regex, contentBlock, (start, end) => {
            const blockKey = contentBlock.getKey();
            const blockSelection = SelectionState
                .createEmpty(blockKey)
                .merge({
                    anchorOffset: start,
                    focusOffset: end,
                });

            selectionsToReplace.push(blockSelection)
        })
    ));

    let contentState = editorState.getCurrentContent();

    selectionsToReplace.forEach((selectionState,i) => {
        contentState = Modifier.replaceText(
            contentState,
            selectionState,
            replace
        )
    });

    return EditorState.push(
        editorState,
        contentState,
    );
}

This is working well alone but when I'm putting mathematics expression using mathjax-plugin, and then I use the Replace function, all the mathematics stuff disappear...

I know that in the definition of the replaceText function we can insert inlineStyle, but I don't found any way to "extract" the style. I tried to use getEntityAt, findStyleRanges, findEntityRanges and others functions but I can't make them do what I want...

Here is my react component :

import React, { Component } from 'react';
import {EditorState, SelectionState, Modifier, convertFromRaw, convertToRaw} from 'draft-js';
import Editor from 'draft-js-plugins-editor';

import createMathjaxPlugin from 'draft-js-mathjax-plugin';
import createMentionPlugin, { defaultSuggestionsFilter } from 'draft-js-mention-plugin';
export default class EditorRplace extends Component {

    constructor(props) {
        super(props);

        this.mentionPlugin = createMentionPlugin({
            entityMutability: 'IMMUTABLE',
            mentionPrefix: '@'
        });

        //We recover the data from the props
        let JSONContentState = JSON.parse(this.props.instruction);
        let inputs = this.props.inputs;
        let values = this.props.values;

        this.state = {
            editorState: EditorState.createWithContent(convertFromRaw(JSONContentState)),
            plugins:[this.mentionPlugin,createMathjaxPlugin({setReadOnly:this.props.isReadOnly})],
            trigger:this.props.trigger,
            inputs:inputs,
            values:values,
        };
    }

    componentDidMount() {
        this.setState({
            editorState:onReplace(this.state.editorState,'A','B')
        })
    }

    onChange = (editorState) => {
        this.setState({
            editorState:editorState,
        })
    };

    render() {
        return (
            <div>
                <Editor
                    readOnly={true}
                    editorState={this.state.editorState}
                    plugins={this.state.plugins}
                    onChange={this.onChange}
                />
            </div>
        );
    }
}

If I'm not using the replace function, everything is displayed as expected, the mention with the right style, and the mathematical expression.


Solution

  • I couldn't found a "proper" solution, then I directly make manual edition on the raw Content State.

    First I'm iterating for all the blocks, then for each entity in this block. I'm replacing manually the text in block[i].text. Then I had to compare the length from the previous element and the new one to change the offset of the next(s) elements.

    I'm using two arrays, one called inputs and one called values. The inputs must be sorted by length (from the higher to the lowest) because if we have @AB and @A and we start by @A we might have a conflict.

    Then each element in the inputs array must have an "index" value which link to the values array to replace properly with the right value.

    let instruction = {"blocks":[{"key":"ar0s","text":"Multiply @alpha by @beta  \t\t ","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[{"offset":9,"length":6,"key":0},{"offset":19,"length":5,"key":1},{"offset":26,"length":2,"key":2}],"data":{}}],"entityMap":{"0":{"type":"mention","mutability":"IMMUTABLE","data":{"mention":{"index":0,"type":"Integer","name":"alpha","min":0,"max":10}}},"1":{"type":"mention","mutability":"IMMUTABLE","data":{"mention":{"index":1,"type":"Integer","name":"beta","min":0,"max":10}}},"2":{"type":"INLINETEX","mutability":"IMMUTABLE","data":{"teX":"\\frac{@alpha}{@beta}","displaystyle":false}}}}
    
    let inputs = [{index:0,name:'alpha'},{index:1,name:'beta'}];
    let values = [1,123456];
    
    replace(instruction,inputs,values);
    
    function replace(contentState,inputs,values)
    {
        instruction.blocks.forEach((block,i) => {
    
        console.log('Block['+i+'] "' + block.text + '"');
    
        let offsetChange = 0;
    
        block.entityRanges.forEach((entity) => {
    
          entity.offset+=offsetChange;
          console.log('\n[Entity] offsetChange:' + offsetChange);
    
          inputs.forEach(input => {
    
          if(instruction.entityMap[entity.key].type === 'mention') {
              if(input.name === instruction.entityMap[entity.key].data.mention.name)
              {
    
                console.log('replace ' + entity.offset + ' ' + entity.length + ' ' + block.text.toString().substr(entity.offset,entity.length));
    
                block.text = block.text.toString().replace(block.text.toString().substr(entity.offset,entity.length),values[input.index])
    
                let newLength = values[input.index].toString().length
                console.log('newLength:' +newLength);
    
                offsetChange+= (newLength-entity.length);
                entity.length=newLength;
    
    
              }
          }
    
          });
    
        });
    
    
      });
    
      return instruction;
    }
    

    So it is working for what I needed.