Draft-js: How to create custom inline components without text

Created on 13 May 2016  路  27Comments  路  Source: facebook/draft-js

I'm currently stuck at creating custom inline components. I want to have a component for inline LaTeX equations. I tried to follow Creating and Retrieving Entities but I'm not sure what to pass Modifier.applyEntity. Something like this didn't work as intended:

``` .js
addInlineLatexEquation() {
const { editorState } = this.state

const entityKey = Entity.create(
'INLINE_LATEX_EQUATION',
'IMMUTABLE',
{ src: '' }
)

const contentState = editorState.getCurrentContent()
const selectionState = editorState.getSelection()

const newContent = Modifier.applyEntity(
contentState,
selectionState,
entityKey
)

const newEditorState = EditorState.push(editorState, newContent, 'apply-entity')

this.setState({
editorState: newEditorState
})
}
```

I now can create an inline LaTeX component by selecting text, but I rather want to have a behavior similar to the block components. I.e., I want to add an inline component at the current cursor position without any text (basically AtomicBlockUtils.insertAtomicBlock for inline components).

Most helpful comment

Just going to share the snippet I shared on Slack. This

  1. Inserts an empty space before the current selection
  2. Adds a blank character with an entity (which I then render customly through a decorator)
  3. Adds a blank character afterwards
import { Entity, Modifier, EditorState } from 'draft-js';

export function insert(editorState) {
  const currentContent = editorState.getCurrentContent();
  const entity = Entity.create(
    'TEX',
    'IMMUTABLE',
    {content: "$ 1 + 1聽$"}
  );

  const selection = editorState.getSelection();

  const firstBlank = Modifier.insertText(
    currentContent,
    selection,
    " ",
    null,
    null
  );

  const withEntity = Modifier.insertText(
    firstBlank,
    selection,
    " ",
    null,
    entity
  );

  const withBlank = Modifier.insertText(
    withEntity,
    selection,
    " ",
    null,
    null,
  );

  return EditorState.push(
    editorState,
    withBlank,
    'insert-text'
  );
}

All 27 comments

Just going to share the snippet I shared on Slack. This

  1. Inserts an empty space before the current selection
  2. Adds a blank character with an entity (which I then render customly through a decorator)
  3. Adds a blank character afterwards
import { Entity, Modifier, EditorState } from 'draft-js';

export function insert(editorState) {
  const currentContent = editorState.getCurrentContent();
  const entity = Entity.create(
    'TEX',
    'IMMUTABLE',
    {content: "$ 1 + 1聽$"}
  );

  const selection = editorState.getSelection();

  const firstBlank = Modifier.insertText(
    currentContent,
    selection,
    " ",
    null,
    null
  );

  const withEntity = Modifier.insertText(
    firstBlank,
    selection,
    " ",
    null,
    entity
  );

  const withBlank = Modifier.insertText(
    withEntity,
    selection,
    " ",
    null,
    null,
  );

  return EditorState.push(
    editorState,
    withBlank,
    'insert-text'
  );
}

@mull Thanks for the example snippet.

If I may ask, what does your TEX entity component look like? I'm having a problem keeping the cursor out of the inline math block, and in turn, letting the user make some weird interactions with the rendered math. I've tried setting contentEditable="false" on the math container, but that has some odd behavior when the math is at the beginning or end of the line.

For reference, this is the super quick and naive component I threw together:

class InlineMath extends Component {
  componentDidMount() {
    const { content } = Entity.get(this.props.entityKey).getData();
    katex.render(content, this.refs.math);
  }

  render() {
    return (
      <span ref="math" contentEditable="false">math</span>
    );
  }
}

// ...

const decorator = new CompositeDecorator([
  {
    strategy: findTex, // essentially find entity.getType() === 'TEX'
    component: InlineMath,
  },
]);

Heads up: right now adding custom text within decorators is not supported. Decorators are only supposed to render what they receive via props. Anything else can result in broken state. There was a discussion about allowing something like atomic decorators, which just render custom content which is not selectable, but then maintaining the selection is tricky.

@jussch You can look at my first attempt at https://github.com/ory-am/editor/tree/draftjs with a live demo at https://inyono.github.io/editor. I basically do the same as in the TeX example by showing a editor for the LaTeX source after clicking the formula. IIRC, it is the same thing @mull is doing.

The 'insert-text' change type had been removed now, and how can I deal with this problem? @hellendag @inyono @mull

I'd like to do the same thing here. Inline equations that are treated as atomic with respect to the cursor, but whose Latex can be edited when clicked on. The only way I can think to do this right now is with decorators but it seems like they wont get me the cursor behavior that I want. Is that true or is there a way for me to treat a particular decorated range as atomic at this point?

@johanneslumpe Did that discussion you referred to ever get resolved?

/cc @hellendag @inyono @mull

I used decorators for this. The cursor behavior is okay but deleting is weird (see https://inyono.github.io/editor/). Didn't find a solution for this, yet.

@afraser no it hasn't yet.

@inyono The cursor behavior in your example is actually rather broken. Using the left/right cursor keys the cursor is jumping to a completely different spot when trying to step through the equation. That's due to the decorator rendering content which is not present in the text. But it should be pointed out that it definitely has this limitation, as opposed to suggesting that the cursor behavior works fine I think.

@johanneslumpe You're right, I mixed up the cursor with the mouse. I guess the weird delete behavior comes from the weird cursor behavior.

@inyono The weird delete behavior and the cursor behavior have the same source, correct: rendering content which is not part of the data model. Draft internally does not know about the content and thus will place the cursor incorrectly and delete different characters.

@inyono @johanneslumpe I noticed if I manually set contenteditable='false' on the Entity span tag, the cursor behavior seems to be restored. Anyone have a sense of whether this is a bad idea?

/cc @aamontal @kevinmook

@jussch Just noticed that you also had some success with setting contentEditable='false' any luck with those anomalies at the beginning and end of line?

@afraser Unfortunately I haven't played around with Draft-JS and contentEditable="false" too much, but I have been playing around with contentEditable="false" within any contentEditable="true". I've found that the anomalies that were occurring in DraftJS were also sort of happening in non-DraftJS contentEditable divs. In Chrome for example, placing the cursor after a <div contentEditable="false"/> at the end of the line renders the cursor off the page.

Outside of those anomalies though, contentEditable="false" seemed to be working fine as long as one character represented one custom inline block, but I haven't played around with it too much yet.

I'm proposing fixes to copy & paste behavior for these sorts of entities here if anyone is interested: https://github.com/facebook/draft-js/issues/504

/cc @inyono @johanneslumpe @jussch @mull

I added an example with working inline equations (including copy & paste) in this PR: https://github.com/facebook/draft-js/pull/525

The only issue I haven't fixed yet is the broken cursor behavior when the equation is at the beginning or end of a block. However, @aamontal has a solution for this working that I'll add to the example sometime next week.

@afraser would love it if you ping this thread when you've got cursor behavior working well!

@mull Can do.

@mull Here's a gist with our editor component in it. The important code for fixing the end-of-line entity bug is in the onChange handler here:

https://gist.github.com/afraser/12d97ee842eeb41bb05dc66fc2c6a9d2#file-boundless-editor-L38-L80

As for copy and paste, you'll have to refer to my PR referenced above.

@afraser much appreciated!

@mull I also found a way to make copy and pasting work without requiring changes to the draft codebase. Here's an example:

https://gist.github.com/afraser/6dedbe486d9a254ab490c2df6ca3abd2

Note, the gist above doesn't include the EOL entity bug fix I sent to you earlier, so you'll have to put those two things together to get the holy grail.

@afraser, fixed a little bug in your gist, where blocks with only plaintext are not copied/pasted.
https://gist.github.com/bryanph/17a805449a0937156965ee55c755ed41

@afraser, I'm now trying to get this to work for pastes between editor (so not internal). No idea how to approach this since the required data is stored in the entity store itself. Do you have any ideas maybe?

The only way I can see to do this is to store the Entity data as a string in the HTML itself as a data- attribute.

@bryanph Thanks for sharing your fix. Pasting between editors is definitely a challenge. I think storing it as a data-attribute is a fine idea but here are a few considerations:

1) You'd be storing the entity data in 2 places, which begs the question: Which is the source of truth? And when are they synced so there is a minimal chance of them being accessed while out of sync.
2) You're better off having one code-path for cloning entities, which means you should rewrite cloneEntitiesInFragment to handle the more general case of pasting from another draft editor.
3) Is it worth avoiding the use of Entities altogether?

Unfortunately I'm no longer working on this project so I most likely won't have much time to make improvements to the code that I've shared, but @aamontal is working on fixing the edge cases that occur when inserting equations at the end of a block.

Thanks @afraser, I will definitely give it a shot.

My main problem now is actually converting from and to html from the inline-latex representation. Using DraftPasteProcessor.processHTML I don't get the same representation unfortunately when trying to convert the external clipboard to ContentBlocks. Going to try some other approaches this week, hopefully I can get something working.

From reading the discussion, it looks like there are some work-arounds that can be used for now to deal with this use case. In general, I don't think we can support having lots of complex custom inline components at this time, so I'll close this issue.

I need inline img.Is there any solution?

Was this page helpful?
0 / 5 - 0 ratings