Say I have an editor and I want to add a colour picker, although this question applies to any style where the list of possibilities is far beyond what is sane to precompute (font size, line height).
We have custom style maps, and can add definitions to it on the fly like so:
{
color_255_255_255_100: {
color: 'rgba(255,255,255,1)',
}
}
Yes there are issues that if we don't update the contentState it won't re-render but we can get around that. The issue is that you are defining separate styles for each colour. Before you apply one you have to manually search the selection for any styles starting with color_, remove them, apply the new one.
Is there a recommended way to do this? How can I be sure I'm not messing the with the pooling on CharacterMetadata?.
Wouldn't an immutable map of react inline styles be better than these constants?
It feels like the current syntax makes these kind of things unnecessarily difficult and inflexible geared towards providing the user with a limited set of predefined options instead of catering to any option and letting the application developer choose if they want to limit it or not.
See #342
@andrewgleave I have seen that but it is a different issue. That still requires you to make tons of entries, that is more about if I have Bold and Italic with a given font... let me override the default behaviour because I don't want to use the faux bold and italic provided by browsers. It doesn't solve the replacing of these styles.
This is more let me choose from 16 million colours in a sane way that doesn't involve ridiculous amounts of parsing.
I would be interested to see how you would implement a color picker with the custom style function, I can only see it having the benefit of not adding the definitions to the custom style map but I still need to pass in styles like color_255_255_255_100 and do a search through my selection for other color_* styles and remove them (which seems to be a lot more verbose than described).
At which point you have to ask what is the point of this abstraction? Why can't I just pass the color prop down with a value and the editor compares it like it does with these labels anyway?
Basically, I store a map of custom styles along side my editor state. When a color is chosen, I create a key based on a constant, the anchor key, selection range and offset e.g. FG_COLOR-5va7g-17. Since the key for a given range will always be the same, I can just set the color in my style map and get the behaviour I need.
Style func is something like:
getStyleFunc(style) {
return this.getEditorStyles().reduce((output, v, k) => style.has(k) ? output.merge(v) : output, Immutable.Map()).toJS();
},
Typing this quickly so let me know if it doesn't make sense!
@andrewgleave Thanks for providing that but how do you handle removing them? I set colour of some text, then select a portion of it along with some other text and set the colour on that. You can't just remove the old one on the part of the text you did highlight surely you need to update the key as the selection range has changed for the previous.
Seems worse than defining keys by colors like color_255_255_255_100.
I think this could be made much simpler by passing in react inline styles, I understand why labels are useful like this for displaying whether header 1 or body is the current style or not, but think this could be added alongside that. For example:
applyDirectInlineStyle(
contentState: ContentState,
selectionState: SelectionState,
style: Object
): ContentState
Pass in a map of react inline styles to take precedence and all of these pre-definable things would be much much simpler.
We can use an Immutable Map of the react styles stored on CharacterMetadata and still take advantage of Immutable's efficient equals() for comparing and pooling.
Pros:
CharacterMetadata pooling. For example:Set block A to red, set block B to green, set block B to red. block A and block B are both red but can't share `CharacterMetadata` because A has `['red']` and B has `['green', 'red']` although green is now redundant (Obviously the names in this example are pre-definable but imagine they were just random RGBA values).
applyDirectInlineStyle(contentState, selectionState, {
color: 'rgba(255, 255, 255, 1)' // replace with variable that holds dynamic colour
});
It's all a hack really, and I have to remove unused styles when I remove them from the editor state. It works, but it's not elegant. You proposal would be a clear clear improvement.
Are you planning to implement, and any idea of how much of the API it would touch?
@andrewgleave I will definitely take a look at implementing this if a team member is willing to weigh in and say whether they are likely to accept this proposal and also whether or not they have considered similar and ran into pitfalls. I'm sure similar has been proposed before but I'm not sure whether it would have been quashed because they favoured the style map or whether there are any serious flaws I am not seeing, in any case I think the two can live quite happily next to each other with direct inline styles having the highest precedence over a given property.
Evaluate style map styles -> merge with direct styles, apply to element.
I haven't touched the codebase yet so not sure about coverage but methods for retrieving these will need to be added and exposed, again it would be good to iron out before hand what and where we need to expose that. I wouldn't expect it to modify any of the existing API but expose new methods on top of it.
Currently I use customStyleMap to dynamically add style in it. But it lags when dragging the color bar.
applyDirectInlineStyle sounds great :tada:
@georeith i'm facing the same use case and i'm considering ditching inline styles entirely for Entities and Decorators. I like your proposal though, can anybody weigh in on this ?
EDIT: Decorators cannot be nested ;/
Cheers
@iandoe https://github.com/facebook/draft-js/issues/246#issuecomment-221120915
here is a way of doing nested decorators.
@georeith actually, this does seem pretty similar to #342, it's essentially the same problem, but with a different proposed API
Hi @georeith. Have you came up with any solution to this problem? I have the same use case. Implementing a color picker, in which the colors can be chosen from a vast amount of options, is a pretty common scenario I think. I was expecting to find an example sooner or later but this issue has broken my heart :(
@damianmr I have solved this by adding inline styles like this:
const CUSTOM_STYLE_PREFIX_COLOR = 'COLOR_';
function customStyleFn(style) {
const styleNames = style.toJS();
return styleNames.reduce((styles, styleName) => {
if(styleName.startsWith(CUSTOM_STYLE_PREFIX_COLOR)) {
styles.color = styleName.split(CUSTOM_STYLE_PREFIX_COLOR)[1];
}
return styles;
}, {});
}
and as a setter in my Rich Text component:
_setColor(color) {
const { editorState } = this.state;
const styles = editorState.getCurrentInlineStyle().toJS();
let newState = styles.reduce((state, styleKey) => {
if(styleKey.startsWith(CUSTOM_STYLE_PREFIX_COLOR)) {
return RichUtils.toggleInlineStyle(state, styleKey);
}
return state;
}, editorState);
if(color != null) {
newState = RichUtils.toggleInlineStyle(newState, CUSTOM_STYLE_PREFIX_COLOR + color);
}
this._onChange(newState);
}
Then you just need to modify your to/from HTML conversion, which I did like this (abridged) with draft-convert:
import { convertFromHTML, convertToHTML } from 'draft-convert';
const contentStateFromHtml = html => convertFromHTML({
htmlToStyle: (nodeName, node, currentStyle) => {
if(node.style == null) {
return currentStyle;
}
if (typeof(node.style.color) === 'string' && node.style.color.length > 0) {
return currentStyle.add(CUSTOM_STYLE_PREFIX_COLOR + node.style.color);
}
if(node.style.textDecoration === 'line-through') {
return currentStyle.add('STRIKETHROUGH');
}
return currentStyle;
}
})(html);
export const editorStateFromHtml = html => EditorState.createWithContent(contentStateFromHtml(html));
export const htmlFromEditorState = editorState => convertToHTML({
styleToHTML: (style) => {
if(style.startsWith(CUSTOM_STYLE_PREFIX_COLOR)) {
const cssDecl = {
color: style.split(CUSTOM_STYLE_PREFIX_COLOR)[1]
};
return <span style={cssDecl} />;
}
if(style === 'STRIKETHROUGH') {
return <del />;
}
}
})(editorState.getCurrentContent());
@davidchang
I don't think it is. #342 is saying that it wants to provide a different style when a combination of styles are applied (it wants better font face support).
I don't believe the proposed API in #342 solves this issue as it still wants to return a stylemap. My issue is that styleMap is great if you can predefine all your styles, otherwise it is a massive hindrance if you can't. Likewise whilst my proposed API could be used to do #342 it might not be ideal (The API at #342 is terrible solution for this issue, it does solve the issue of refreshing styleMap but using styleMap still creates a myriad of other issues). So it's important that they are viewed as separate.
@awestroke great solution, I was thinking I would need to do something like that but I am just starting with the Draft API and I am kind of lost yet. Thank you!!
I created a package to help with creating dynamic inline styles, you can check it out here
https://www.npmjs.com/package/draft-js-custom-styles
demo:
https://webdeveloperpr.github.io/draft-js-custom-styles/
The main trouble of DraftJS styles. That this one can make toggle styles but can't make addStyle or deleteStyle
But actually in the file _RichTextEditorUtil.js_ we can see at the simple logic that means _DraftModifier.removeInlineStyle_ and _DraftModifier.applyInlineStyle_
My decision is just copy and paste that code into my project, and it works.
But main question is still opened. WHY can't you (facebook draftjs team) make not only toggle sytle but add and remove style?
Most helpful comment
@damianmr I have solved this by adding inline styles like this:
and as a setter in my Rich Text component:
Then you just need to modify your to/from HTML conversion, which I did like this (abridged) with
draft-convert: