facebookarchive / draft-js

A React framework for building text editors.
https://draftjs.org/
MIT License
22.58k stars 2.64k forks source link

Block splitting doesn't preserve block meta data #723

Open N1kto opened 8 years ago

N1kto commented 8 years ago

Do you want to request a feature or report a bug? Bug What is the current behavior? When splitting a ContentBlock its data prop is not copied to a new ContentBlock. If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. You can use this jsfiddle to get started: https://jsfiddle.net/stopachka/m6z0xn4r/. Create a custom block with some block data. Then split it with enter key. Inspect the new block - its data prop is empty. What is the expected behavior? Data content from original block should be copied to new block. Same is done with Entity data. Which versions of Draft.js, and which browser / OS are affected by this issue? Did this work in previous versions of Draft.js? Draft version 0.9.1. It doesn't depend on browser / OS. I think it has always been so, especially taking into account that block data prop is relatively a new feature.

jan4984 commented 8 years ago

is there any reason why should we copy meta data to split block?

N1kto commented 8 years ago

Not to split, but upon split. Yes, I find it needed. Similarly to Entity data which is kept upon split in both blocks the meta data should be also kept.

In my use case block meta data influences the presence of particular block in editor. So, once I split the block the part of text gets lost since new block created upon split doesn't hold needed meta data. It can be any other appearance-related issues. For example, block text alignment (left | center | right) data can be kept in its meta data, so once you split the text - you expect a new line to be aligned similarly to original block, but it won't.

I have found a workaround to tackle this issue, but it would be great if block data would be kept by default.

michelson commented 8 years ago

this could be related to #718 ?

icd2k3 commented 7 years ago

I have a similar problem where a user can add a tag to a line of text in a list, this tag is stored in block meta-data, but if I move that line to a new line all of that meta-data stays at the first line... example:

- First line text [mock_ui_tag]

now I move my cursor to the first 0 position of that line and press enter to create a new block... now I see:

- [mock_ui_tag]
- First line text

Ideally I want the tag to follow the text. Here's a gif example as well: gif

sudkumar commented 6 years ago

Hi @N1kto. I need this for textAlignment functionality. Can you please provide your workaround solution for this issue. I'm thinking of using handleReturn api for this.

N1kto commented 6 years ago

@sudkumar please see below. It's in CoffeScript, but I don't think is it a problem. Please note that I wrote it a good while ago, DraftJS APIs it uses might have changed since then.

  splitBlock: (editorState) ->
    newContent = Modifier.splitBlock editorState.getCurrentContent(), editorState.getSelection()
    originalBlockData = newContent.blockMap.get(newContent.selectionBefore.getStartKey()).getData()
    newBlockKey = newContent.selectionAfter.getStartKey()
    newBlockMap = newContent.blockMap.update newBlockKey, (contentBlock) ->
      contentBlock.set 'data', originalBlockData

    EditorState.push editorState, newContent.set('blockMap', newBlockMap), 'split-block'
icd2k3 commented 6 years ago

Thanks @N1kto! I was able to modify your response to suit my problem demonstrated in the gif a couple replies up ^

gif

I REALLY wish this was supported out of the box with a param in draft preserveBlockDataOnSplit or something, but for now this workaround should keep me moving. It's a bit verbose, but if anyone has similar block "tagging" functionality in your UI this should work as well

handleReturn() {
    const { editorState } = this.state;
    const contentState = editorState.getCurrentContent();
    const selectionState = editorState.getSelection();

    let newContent = Modifier.splitBlock(contentState, selectionState);

    const originalBlock = contentState.blockMap.get(newContent.selectionBefore.getStartKey());
    const originalBlockData = originalBlock.getData();
    const preserveDataOnSplit = originalBlockData.size
      && selectionState.getFocusOffset() === 0;

    /**
    /* If the user is trying to bump a block down a row that _has_ data (like
    /* attributions) we need to manually transfer that data along with the
    /* block split. This code can maybe be eventually deleted if draft decides to
    /* support this use case out of the box.
    */
    if (preserveDataOnSplit) {
      const newBlockKey = newContent.selectionAfter.getStartKey();

      // clear data out of the current block we're moving down
      newContent = Modifier.setBlockData(newContent, selectionState, {});

      // create the new block and transfer data from the current block
      const newBlockMap = newContent.blockMap.update(newBlockKey, contentBlock =>
        contentBlock.set('data', originalBlockData));
      newContent = newContent.set('blockMap', newBlockMap);

      // note: handleChange and getNewEditorState are just my helper methods
      this.handleChange(getNewEditorState({
        editorState,
        contentState: newContent,
      }));

      /**
      /* now that the block is split and the data has been transfered
      /* we need to force the cursor one block down. Unfortunately it looks
      /* as though there is a limitation in editorState where we have to do
      /* this as a subsequent change
      */
      const newSelection = new SelectionState({
        anchorKey: newBlockKey,
        anchorOffset: 0,
        focusKey: newBlockKey,
        focusOffset: 0,
      });
      setTimeout(() => {
        this.setState({
          editorState: EditorState.forceSelection(this.state.editorState, newSelection),
        });
      }, 1);

      return 'handled';
    }

    return 'not-handled';
  }
sudkumar commented 6 years ago

Thanks you @N1kto . That helped. I only wanted to pass-on the styles which I stored in blockData.style. I did following.

    handleReturn = (e, editorState) => {
        const selection = editorState.getSelection()
        if (selection.isCollapsed()) {
            const contentState = editorState.getCurrentContent()
            const startKey = selection.getStartKey()
            const currentBlock = contentState.getBlockForKey(startKey)
            if (currentBlock) {
                const blockData = currentBlock.getData()
                const blockStyles = blockData.get("style")
                if (blockStyles) {
                    // Original split logic
                    const newContentState = Modifier.splitBlock(
                        editorState.getCurrentContent(),
                        editorState.getSelection()
                    )
                    let splitState = EditorState.push(editorState, newContentState, "split-block")
                    const target = splitState.getSelection()
                    const afterMergeStylesContentState = Modifier.mergeBlockData(
                        splitState.getCurrentContent(),
                        target,
                        {
                            style: blockStyles
                        }
                        )
                    splitState = EditorState.push(editorState, afterMergeStylesContentState, "split-block")
                    this.updateState(splitState)
                    return "handled"
                }
            }
        }
        return "not-handled"
    }
punitjajodia commented 5 years ago

I had to do something similar, but in my case, instead of copying over the data from the previous block, I had to assign new metadata to the newly split block.

const { editorState } = this.state;
const selection = editorState.getSelection();
let contentState = editorState.getCurrentContent();

contentState = Modifier.splitBlock(contentState, selection);

const nextBlockKey = contentState.selectionAfter.getStartKey();

const nextBlockEmptySelection = new SelectionState({
  anchorKey: nextBlockKey,
  anchorOffset: 0,
  focusKey: nextBlockKey,
  focusOffset: 0
});

contentState = Modifier.setBlockData(
  contentState,
  nextBlockEmptySelection,
  Map()
    .set("createdTime", Date.now())
    .set("creator", "Punit Jajodia")
);

const newEditorState = EditorState.set(editorState, {
  currentContent: contentState
});