Draft-js: `customStyleMap` is too limiting

Created on 27 Apr 2016  路  24Comments  路  Source: facebook/draft-js

customStyleMap works by identifying inline styles and iteratively building a style map by concatenating the maps together in order. For example, a character with BOLD and ITALIC would apply font-weight: bold and font-style: italic, which works well for thise case.

However, sometimes, you want to apply a single style based on having two overlapping ranges. For example, assuming the ordered set ['BOLD', 'ITALIC'] I want:

style: {
  fontFamily: 'MyFont-BoldItalic';
}

This is because I don't control the weights via CSS transforms alone, but a custom font.

I thought of a couple hacky solutions:

  1. Define and apply a new custom style BOLD_ITALIC when I detect overlapping ranges, and use the customStyleMap but this just feels like sadness.
  2. Use a decorator, but sadly, I would have to switch entirely to using a decorator for single styles because otherewise `customStyleMap``creates a new, styled span within the decorator's span, overriding styles.

The better solution, I think, may be to allow for a customStyleFn which takes a character and returns the styleMap you want to apply. This means that you'd lose the ability to "stack" styles via customStyleMap. Maybe this is a bit too heavy for this use case, but I'm not seeing anything more elegant right now.

question

Most helpful comment

The customStyleFn approach is interesting. I don't think you'd have to lose the ability to stack styles, you'd just have to do the combinations yourself.

An added benefit of this function would be avoiding the need to create a new customStyleMap object every time a new style name is added. This would be especially helpful for cases like color pickers.

Thinking aloud...

function customStyleFn(style: DraftInlineStyle): Object {
  const output = {};
  const isBold = style.has('BOLD'):
  const isItalic = style.has('ITALIC');

  if (isBold) {
    if (isItalic) {
      output.fontFamily = 'MyFont-BoldItalic';
    } else {
      output.fontWeight = 'bold';
    }
  } else if (isItalic) {
    output.fontStyle = 'italic';
  }

  // A possibility for color picker style names...
  const color = style.filter((value) => value.startsWith('#')).first();
  if (color) {
    output.color = color;
  };

  // etc.
  return output;
}

Is this something like what you have in mind?

The library could provide a default customStyleFn to process existing maps and ease the transition.

All 24 comments

The customStyleFn approach is interesting. I don't think you'd have to lose the ability to stack styles, you'd just have to do the combinations yourself.

An added benefit of this function would be avoiding the need to create a new customStyleMap object every time a new style name is added. This would be especially helpful for cases like color pickers.

Thinking aloud...

function customStyleFn(style: DraftInlineStyle): Object {
  const output = {};
  const isBold = style.has('BOLD'):
  const isItalic = style.has('ITALIC');

  if (isBold) {
    if (isItalic) {
      output.fontFamily = 'MyFont-BoldItalic';
    } else {
      output.fontWeight = 'bold';
    }
  } else if (isItalic) {
    output.fontStyle = 'italic';
  }

  // A possibility for color picker style names...
  const color = style.filter((value) => value.startsWith('#')).first();
  if (color) {
    output.color = color;
  };

  // etc.
  return output;
}

Is this something like what you have in mind?

The library could provide a default customStyleFn to process existing maps and ease the transition.

Yes, that's exactly what I was leaning towards.

(I wasn't clear by what I meant with the ability to stack styles, I meant you wouldn't get it "for free" and you'd have to implement yourself as you did in the code example.)

That would be great. I'm in a similar position: needing to implement a color picker and custom font styles.

Is this something that, if implemented, would likely be merged, and does anyone have plans to work on it?

If not, I'll take a look since I have a deadline and don't want to have to drop Draft because I can't implement a color picker.

Nothing prevents you from implementing a color picker. That said I was
planning to work on this next week. But have at it.

@andrewgleave I'd certainly be open to a PR.

I think a color picker should be doable already, though with some awkwardness around forcing re-renders.

I presume you mean that I could just mutate customStyleMap and force a re-render to get the behaviour? Or have I missed something?

If I get time, I'll be looking at this early next week.

@andrewgleave: Exactly. If you add new values to your customStyleMap, and relevant ContentBlock objects have the new style strings, then re-rendering the editor should re-render those blocks accordingly.

The awkwardness arises when making changes to the customStyleMap without corresponding changes to the ContentState, which would necessitate forcing a re-render of the editor.

Yeah, that's what I'm/was doing.

I made a start late today on adding support for customStyleFn. Will let you know.

@andrewgleave check out the referenced PR above.

@guitardave24 Excellent!

I have a similar issue, but would like to be able to create customRenderMap. The reason for that is that I would like to be able to create a custom inline style with a className that I can manipulate in an CSS file.

My use case is that I want to use customRenderMap to create <redact className="redact" /> and manipulate it using a pseudo selector.

.redact,
.redact::selection {
  color: rgb(0, 0, 0);
  background: rgb(0, 0, 0);
  transition: background 1s;
}

.redact:hover {
  background: rgb(50, 50, 50);
}

Any particular reason this has not been merged yet ? cheers

Yeah, seems to be good.

Ditto

@hellendag ?

What about something like this for block styles as well?

For example, storing the amount of spacing before/after a block in the block-level metadata and then converting that dynamically to a React style object with the appropriate marginTop and marginBottom values.

I know something like this can already be done with blockStyleFn but that requires predefined classes. So if, for example, I wanted margin options for 4px, 5px, 6px... 19px, 20px, I would have to create a separate CSS class for each of them.
Would be much nicer to just return { ... marginTop: data.spaceBefore, ... } from a function.

Hmm, perhaps I should create a separate issue for that :)

I have proposed an additional enhancement to customStyleFn in a PR here: https://github.com/facebook/draft-js/pull/851 . This allows you to specify different CSS objects for an inline style depending on the underlying block type. Hopefully this is functionality others would find useful in master, as well.

I created an npm package that helps out with adding customStyles .
https://www.npmjs.com/package/draft-js-custom-styles

styles

I have a use case that isn't covered by the addition of customStyleFn: using the ::before and ::after pseudo selectors, because they aren't supported by React.

My initial take was that the best solution would be allowing mapping to classes, so that you can map a style to a class - this would solve the issue for me and would provide plenty of flexibility for others as well (including how to handle multiple applied styles).

It seems that the customStyleFn almost tries to re-implement some of what CSS is supposed to do already.

My specific case is making it possible to add quotes as a style, so styled ranges appear inside quotes.

Any thoughts?

@mzedeler you might want to have a look at #957 if you haven't already.

Otherwise, for your use case you should be able to create a decorator with a strategy based on findStyleRanges and then set a class there with the pseudo elements.

I'm having a trouble using customStyleFn. When the values of customStyleFn have changed, I have to re-render the editor itself to make changes across all over the content simultaneously. I have no idea about re-rendering Draft editor. How can I do that?

Nevermind. I found a solution. Thanks to @thesunny (https://github.com/facebook/draft-js/issues/458#issuecomment-225710311) and @philraj (https://github.com/facebook/draft-js/issues/458#issuecomment-272531222). Get an editor state like:

    const selectedEditorState = EditorState.forceSelection(
      editor.editorState,
      editor.editorState.getSelection()
    );

And force re-render the editor by :

function forceRender() {
  const currentState = store.getState();
  const content = currentState.editor.editorState.getCurrentContent();
  const newEditorState = EditorState.createWithContent(content, new CompositeDecorator([]));

  store.dispatch({
    type: UPDATE_EDITOR_STATE,
    payload: newEditorState
  });
}

I can get a re-rendered editor.

Was this page helpful?
0 / 5 - 0 ratings