-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Need ability to "not continue" an inline style or mutable entity #430
Comments
I haven't looked at exactly how it's implemented, but if you go to the example on the homepage https://facebook.github.io/draft-js/, click Bold, type some text, click Bold again, then type more, the second piece of text is not bold. Is that what you want? |
Hm, that is a good point. I suppose applying a style to the collapsed, new range may solve it, though I am certain I tried that. Let me see. Thanks @spicyj |
Ah, yes. It uses toggleInlineStyle which uses I've switched to using this as it's exposed. Thanks for the tip. |
Actually, re-opening because I think it's still valid. In the above case, I actually never want it to continue from the edge. Otherwise, you end up with weird edge cases such as being unable to get out of it without some hotkey or UI affordance. I can solve the case for continuing to type after the transformation, but once I move the cursor left and right again (for example), it picks up the style and you can't get out of it. |
Are you sure you never want to continue the style from the edge? If the user toggles a style override and begins typing, the cursor remains at the edge of the styled span -- shouldn't that override continue to be valid as the user continues to type? Do you have examples of rich editors that support this behavior without a UI affordance or key command? I think your use case makes sense, I just want to evaluate whether this is something that should be fixed at a framework level, or whether the current APIs allow the appropriate behavior. |
To answer your question, yes, that's correct -- it should be valid. I handle this case, and it's a little hairy. The code hasn't been battle-tested yet, but the basic idea seems to work. As for the punchline, I'm not sure whether or not it should be made available at the framework level or not. It seems fairly isolated, but this will require ongoing maintenance as the framework progresses. I don't have the historical knowledge to make a judgment call here, but happy to take a shot at integrating. I don't have a specific example in mind. Medium (for example), does what I am doing here, which is that a new block is a "reset" and it doesn't look upward. But it doesn't have inline code snippets like this. handleBeforeInput(chars, {getEditorState, setEditorState}) {
const state = getEditorState();
const selection = state.getSelection();
if (!selection.isCollapsed()) {
return false;
}
const startOffset = selection.getStartOffset();
const block = getCurrentBlock(state);
let style = state.getInlineStyleOverride();
if (style === null) {
style = block.getInlineStyleAt(startOffset);
}
let insertManually = false;
if (startOffset == 0) {
// We check the style here because Draft's inherent behavior is such that if there
// is no inline style, it "carries" from the previous line or block. We want it to
// "reset" for each block. However, if there is a style override, we should
// let the default behavior occur.
insertManually = style.isEmpty();
} else if (style.intersect(rigidInlineTypes).isEmpty()) {
// Some inline types should not be continuable at the far edge. That is,
// if you put the cursor at the end of the style and start typing, it should
// not carry that style. Draft does this be default, so we need to defeat it
// just as we do above.
const prevStyle = block.getInlineStyleAt(startOffset - 1);
// Check if we're bordering on a rigid type.
insertManually = !prevStyle.intersect(rigidInlineTypes).isEmpty();
}
if (insertManually) {
const entity = block.getEntityAt(startOffset);
// Insert the text manually so we can control the inline style behavior.
let content = state.getCurrentContent();
content = Modifier.insertText(content, selection, chars, style, entity);
setEditorState(EditorState.push(state, content, 'insert-characters'));
return true;
}
return false;
} Of course, this has ramifications. For example, the default behavior of toggleInlineStyle will not work with this implementation, as I have to handle the isCollapsed case myself: /**
* This is our own custom version of toggle inline style. It has one special
* case where we want to toggle a style at the start of the block. The default
* behavior for Draft is to look upward to a previous block, but we don't
* want that, so we manually set the override.
*/
export function toggleInlineStyle(state, inlineStyle) {
const selection = state.getSelection();
if (selection.isCollapsed()) {
const currentStyle = (selection.getFocusOffset() == 0)
? getCurrentBlock(state).getInlineStyleAt(0)
: state.getCurrentInlineStyle().subtract(rigidInlineTypes);
return EditorState.setInlineStyleOverride(
state,
currentStyle.has(inlineStyle)
? currentStyle.remove(inlineStyle)
: currentStyle.add(inlineStyle));
}
// Fallback to default beahvior.
return RichUtils.toggleInlineStyle(state, inlineStyle);
} |
And sorry, to be clear, there are a couple things going on in this code (sorry for conflating):
|
We have this need as well. |
I'm not sure if our use-case warrants a different issue than this one because it seems to be very related so I'll just ask here. We're applying a How can we achieve such an effect? I thought of appending a whitespace character with no entity to the block's charlist but it seems hackish and has side-effects on the UI as the user shouldn't see a space since they didn't insert one... 😕 Another thought was to provide a custom strategy (or just tune the Would love your input on this, kind of stuck on this now. Thank you very much. |
@hellendag @amireh I am facing the same issue, creating a link that i want to be editable inside but non contiguous. We should add some kind of Attached PR #510 |
Sometimes, we do not want MUTABLE entities to be continued when they are on the left of the cursor, for example when creating a LINK entity that we do not want to continue after pressing the space key, this proposal adds a "Contiguous" setting set to true by default that we can set to false to get the desired behavior
Update, i made it work in #510 Grunt work is there: if (offset > 0) {
entityKeyBeforeCaret = contentState.getBlockForKey(key).getEntityAt(offset - 1);
entityKeyAfterCaret = contentState.getBlockForKey(key).getEntityAt(offset);
caretOutsideEntity = (entityKeyBeforeCaret !== entityKeyAfterCaret);
return filterKey(entityKeyBeforeCaret, caretOutsideEntity);
} function filterKey(
entityKey: ?string,
caretOutsideEntity: ?bool,
): ?string {
if (entityKey) {
var entity = DraftEntity.get(entityKey);
// if entity is mutable and caret is inside it, return it
if (entity.getMutability() === 'MUTABLE' && !caretOutsideEntity) {
return entityKey;
}
// entity is mutable and the caret is outside of the entity
// if it is contiguous, return it, else null
if (entity.getMutability() === 'MUTABLE' && caretOutsideEntity) {
return (entity.getContiguity()) ? entityKey : null;
}
}
return null;
} There's a few todo's left and i'm busy with work so if anybody can help out on what's left so we can get this merged in sooner rather than late i'd be grateful :) |
I was thinking about adding a zero width space as first character in the desired entity type and right after outside of it so that moving the cursor can "enter/leave" the entity. So when pressing left/right at entity edges the cursor wouldn't visually move but its position would change and allow typing before/after outside and at start/end inside of entity. Is this an acceptable hack? |
@davidbyttow Hi, davidbyttow. I have the same need with you. Thank you for your code above and now I'm using it . Just find that "getCurrentBlock" and "rigidInlineTypes" is not mentioned in documentation. I think that "getCurrentBlock" is implemented by yourself, but I really have no idea about how to get current block and what "rigidInlineTypes" is. Could you help me? |
Now I find "getBlockForKey" in documentation and implement my "getCurrentBlock". So, I still don't know what "rigidInlineTypes" is. Is it a string or something?
|
Here is my workaround (used like a plugin in draft-js-plugins), which overrides this behavior only for entities: handleBeforeInput: (chars, { getEditorState, setEditorState }) => {
const state = getEditorState();
const selection = state.getSelection();
if (!selection.isCollapsed()) {
return false;
}
const startOffset = selection.getStartOffset();
const content = state.getCurrentContent();
const block = content.getBlockForKey(selection.getStartKey());
const entity = block.getEntityAt(startOffset);
if (entity === null) {
const style = state.getCurrentInlineStyle();
const newContent = Modifier.insertText(content, selection, chars, style, null);
setEditorState(EditorState.push(state, newContent, 'insert-characters'));
return 'handled';
}
return 'not-handled';
} |
Here is how I have implemented the logic for MUTABLE entity which are not contiguous. Entity creation // create the entity which is mutable but not contiguous
const data = { href: "https://www.domain.com" }
contentState.createEntity(
"LINK",
"MUTABLE",
{ ...data, url: data.href, target: "_blank", contiguous: false }
) Utility to handle the contiguous entity import {
EditorState,
Modifier
} from "draft-js"
// method to handle the contiguous entity for the newly inserted chars in the editor state
const handleContiguousEntity = (chars, editorState) => {
const selectionState = editorState.getSelection()
let contentState = editorState.getCurrentContent()
const startKey = selectionState.getStartKey()
const block = contentState.getBlockForKey(startKey)
// handle the extending of mutable entities
if (selectionState.isCollapsed()) {
const startOffset = selectionState.getStartOffset()
if (startOffset > 0) {
// we are not at the start of the block
const entityKeyBeforeCaret = block.getEntityAt(startOffset - 1)
const entityKeyAfterCaret = block.getEntityAt(startOffset)
const isCaretOutsideEntity = (entityKeyBeforeCaret !== entityKeyAfterCaret)
if (entityKeyBeforeCaret && isCaretOutsideEntity) {
const entity = contentState.getEntity(entityKeyBeforeCaret)
const isMutable = entity.getMutability() === "MUTABLE"
const { contiguous = true } = entity.getData()
// if entity is mutable, and caret is outside, and contiguous is set the false
// remove the entity from the current char
if (isMutable && !contiguous) {
// insert the text into the contentState
contentState = Modifier.insertText(
contentState,
selectionState,
char,
editorState.getCurrentInlineStyle(),
null
)
// push the new content into the editor state
const newEditorState = EditorState.push(
editorState,
contentState,
"insert-characters"
)
return newEditorState
}
}
}
}
return false
} The Editor Component's handleBeforeInput prop // method inside the editor component
handleBeforeInput = (char) => {
const { editorState } = this.state
const newEditorState = handleContiguousEntity(char, editorState)
if (newEditorState) {
this.setState({ editorState: newEditorState })
return "handled"
}
return "not-handled"
} |
@tomconroy Thank u so much! I'm very happy to get your solution |
I use @sudkumar piece of code and implement it for my needs.. So if someone wants to look on that code and find inspiration how to handle some extra cases like convert HTML to State with extra Entity data etc... you will find it here: https://gist.github.com/bultas/7f551efbc9054f8cf227f42db55eec07 |
@tomconroy I don't think |
In my testing of other editors, lots/all have the behavior to continue inline styles, but none continue inline mutable entities. IMHO this issue should be restricted to this case and marked as a bug, unless there is a use case I'm overlooking where it's desirable to continue entities. Note: tested Google Docs, Dropbox Paper, Gmail, Apple Pages. |
handleBeforeInput = (chars, editorState) => {
const selection = editorState.getSelection()
const startOffset = selection.getStartOffset();
const content = editorState.getCurrentContent();
const block = content.getBlockForKey(selection.getStartKey());
const entity = block.getEntityAt(startOffset);
if (entity === null) {
const style = editorState.getCurrentInlineStyle();
const newContent = Modifier.insertText(content, selection, chars, style, null);
const newEditorState = EditorState.push(editorState, newContent, 'insert-characters');
this.setState({ editorState: newEditorState });
return 'handled';
}
return 'not-handled';
} |
I just implemented this in the |
I'm surprised the v0.10.5 changelog doesn't mention this issue, this got fixed for entities:
For inline styles, still works the same as before 🙂. |
@thibaudcolas thank you so much 👍 it works after updating to v10.0.5. |
@haikyuu not really since you still have to use the workaround I posted to get it working for inline styles? |
@mxstbr i think it's the expected behaviour for inline styles. Check notes app, email app ... in mac |
The point is that it depends on how users can type inline styles. If you have an inline toolbar, sure the current behavior makes sense, but if you have markdown shortcuts it's very annoying since you can't exit an inline style at the end of the editor. That's why we need this behavior to be configurable, so you can set it to work the way it makes sense in your application! Nobody's saying to change it to only be non-sticky, but it should be configurable out of the box. |
Is there a recommendation for the best way to get the previous contiguous entity behavior? For my entities use case, that was the preferred behavior so the 0.10.5 upgrade is posing some issues. |
@skinandbones you should be able to implement this in Something like: handleBeforeInput(char) {
const { blockTypes } = this.props;
const { editorState } = this.state;
const selection = editorState.getSelection();
if (selection.isCollapsed()) {
const block = getSelectedBlock(editorState);
if (hasSelectionStartEntity(selection, block)) {
this.onChange(
insertTextWithEntity(editorState, char),
);
return HANDLED;
}
}
return NOT_HANDLED;
}
hasSelectionStartEntity(selection, block) {
const startOffset = selection.getStartOffset();
return block.getEntityAt(startOffset) === ! null;
},
insertTextWithEntity(editorState, text) {
const content = editorState.getCurrentContent();
const selection = editorState.getSelection();
const style = editorState.getCurrentInlineStyle();
const entity = block.getEntityAt(selection.getStartOffset());
const newContent = Modifier.insertText(
content,
selection,
text,
style,
entity,
);
return EditorState.push(editorState, newContent, 'insert-characters');
}, Taken from the implementation of the inverse behavior over at springload/draftail#106. |
Right now, the editor simply looks to the left of the cursor to figure out what the style should be when inserting text (https://github.com/facebook/draft-js/blob/master/src/model/immutable/EditorState.js#L597). This works for "continuing" bold/italic/etc which is usually what you want. But sometimes you might not. It also uses the similar logic for mutable entities.
This might be complicated to describe, so I will explain with a use case: Imagine you want to implement inline code
like this
. So when you type a tick, some text, and another tick, it converts 'foo' intofoo
, by replacing the text and applying an inline style. However, as you continue typing, it will extend that code snippet because the cursor is picking up the inline style.There are workarounds like: using immutable/segmented entities (mutable also exhibit this behavior) or adding an extra space when replacing the text and applying the inline code. But, really, what I want is the ability to edit "inside" of the snippet, but not have it continue at the boundaries.
I currently have a hacky workaround which handles
onBeforeInput
globally for certain inline styles/entities and manually inserts text when it borders on one of these types of inline styles.Not sure if this is a widespread, known or otherwise specific use case, but worth logging.
The text was updated successfully, but these errors were encountered: