I don't know if this is a mon issue or a mistake on our part but maybe someone has an idea: We're building an HTML editor with react and slate. The User can select a textbox and then change attributes. This works fine for simple buttons. However, when I open a dropdown (react-select) to for example change font size, the selected text is no longer marked. Slate keeps the selection so the changes take effect but it's a bad UX like that.
Imho this should be a slate feature to keep the text marked but maybe it's something I need to apply myself.
some snippets, don't know if they help:
The Editor ponent initializes the font style plugins and takes care of serialization.
class Editor extends React.Component {
constructor(props) {
super(props);
this.config = {
...mergePluginConfig(PLUGIN_CONFIG, props),
getEditor: () => this.editor,
getValue: () => this.state.value,
};
this.plugins = initializePlugins(this.config);
this.htmlSerializer = new HtmlSerializer({
rules: getSerializationRulesFromPlugins(this.plugins),
});
this.schema = getSchemaFromPlugins(this.plugins);
this.state = {
value: this.htmlSerializer.deserialize(props.value)
};
ref = editor => {
this.editor = editor;
};
render() {
return (
<div>
<Toolbar>
<div className="control">
{renderToolbarElementWithPlugins(this.plugins, 'font-size')}
</div>
<!--- more tools --->
<SlateEditor
autoFocus={true}
spellCheck={true}
placeholder={this.props.placeholder}
ref={this.ref}
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
plugins={this.plugins}
schema={this.schema}
/>
onChange = ({ value }) => {
const {startInline, endInline, document, selection, fragment} = value;
// holds the correct information
console.log(fragment.text);
// ...
this.setState({ value });
this.props.onChange(this.htmlSerializer.serialize(value));
};
This is the font-size plugin that is initialized with the others and will be displayed in the toolbar:
export default function initializeFontSizePlugin(options) {
// this takes care of detecting the current value and applying selected change to the value.
// it does not change selection
const plugin = createStyleBasedMarkPlugin(...);
const fontSizeOptions = options.fontSizeOptions || [];
const handleFontSizeChange = ({value}) => {
plugin.reapplyFontSize({value: rendererFontSize(value)});
};
return {
...plugin,
renderToolbarElement() {
const {isMixed, fontSize} = plugin.detectFontSize();
return <Select
isCreatable={true}
name='font-size'
value={isMixed ? undefined : displayFontSize(fontSize)}
onChange={handleFontSizeChange}
options={fontSizeOptions}
/>;
}
};
}
My current solution is to focus slate as soon as select opens and then tell select to be open but that feels hackish and has drawbacks (see below)
const handleFontSizeChange = ({value}) => {
plugin.reapplyFontSize({value: rendererFontSize(value)});
handleMenuClose();
};
let menuIsOpen = false;
let firstOpen = false;
const handleMenuOpen = (editor) => {
firstOpen = true;
if(!menuIsOpen) {
setTimeout(() => {
if (editor) {
editor.focus();
}
menuIsOpen = true;
}, 1);
}
}
const handleMenuClose = (editor) => {
if(!firstOpen) {
setTimeout(() => {
if(menuIsOpen) {
menuIsOpen = false;
if (editor) {
editor.focus();
}
}
}, 1);
} else {
firstOpen = false;
}
}
<Select
onMenuOpen={handleMenuOpen.bind(this)}
onMenuClose={handleMenuClose.bind(this)}
menuIsOpen={menuIsOpen}
I have to use the timeout to get outside the react lifecycle and I have to add an additional flag since losing focus on the select ponent will also close it. Like I said that has drawbacks: - a little flicker on the selected text during the focus switch - the dropdown box in select has the wrong coloring (not focused obviously) - switching to another dropdown (like alignment) won't close the other since that already has no focus:
Additional info:
We have to work with slate and slate-react
at version 0.47 as higher versions are not supported by slate-html-serializer
which we need. Maybe this has already been solved in a higher version?
So, I have a somewhat working version but I'd much more prefer a solution where slate takes care of the selection "natively" without me having to handle focus. It should be possible I think without the selected text flickering
and off colors.
I don't know if this is a mon issue or a mistake on our part but maybe someone has an idea: We're building an HTML editor with react and slate. The User can select a textbox and then change attributes. This works fine for simple buttons. However, when I open a dropdown (react-select) to for example change font size, the selected text is no longer marked. Slate keeps the selection so the changes take effect but it's a bad UX like that.
Imho this should be a slate feature to keep the text marked but maybe it's something I need to apply myself.
some snippets, don't know if they help:
The Editor ponent initializes the font style plugins and takes care of serialization.
class Editor extends React.Component {
constructor(props) {
super(props);
this.config = {
...mergePluginConfig(PLUGIN_CONFIG, props),
getEditor: () => this.editor,
getValue: () => this.state.value,
};
this.plugins = initializePlugins(this.config);
this.htmlSerializer = new HtmlSerializer({
rules: getSerializationRulesFromPlugins(this.plugins),
});
this.schema = getSchemaFromPlugins(this.plugins);
this.state = {
value: this.htmlSerializer.deserialize(props.value)
};
ref = editor => {
this.editor = editor;
};
render() {
return (
<div>
<Toolbar>
<div className="control">
{renderToolbarElementWithPlugins(this.plugins, 'font-size')}
</div>
<!--- more tools --->
<SlateEditor
autoFocus={true}
spellCheck={true}
placeholder={this.props.placeholder}
ref={this.ref}
value={this.state.value}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
plugins={this.plugins}
schema={this.schema}
/>
onChange = ({ value }) => {
const {startInline, endInline, document, selection, fragment} = value;
// holds the correct information
console.log(fragment.text);
// ...
this.setState({ value });
this.props.onChange(this.htmlSerializer.serialize(value));
};
This is the font-size plugin that is initialized with the others and will be displayed in the toolbar:
export default function initializeFontSizePlugin(options) {
// this takes care of detecting the current value and applying selected change to the value.
// it does not change selection
const plugin = createStyleBasedMarkPlugin(...);
const fontSizeOptions = options.fontSizeOptions || [];
const handleFontSizeChange = ({value}) => {
plugin.reapplyFontSize({value: rendererFontSize(value)});
};
return {
...plugin,
renderToolbarElement() {
const {isMixed, fontSize} = plugin.detectFontSize();
return <Select
isCreatable={true}
name='font-size'
value={isMixed ? undefined : displayFontSize(fontSize)}
onChange={handleFontSizeChange}
options={fontSizeOptions}
/>;
}
};
}
My current solution is to focus slate as soon as select opens and then tell select to be open but that feels hackish and has drawbacks (see below)
const handleFontSizeChange = ({value}) => {
plugin.reapplyFontSize({value: rendererFontSize(value)});
handleMenuClose();
};
let menuIsOpen = false;
let firstOpen = false;
const handleMenuOpen = (editor) => {
firstOpen = true;
if(!menuIsOpen) {
setTimeout(() => {
if (editor) {
editor.focus();
}
menuIsOpen = true;
}, 1);
}
}
const handleMenuClose = (editor) => {
if(!firstOpen) {
setTimeout(() => {
if(menuIsOpen) {
menuIsOpen = false;
if (editor) {
editor.focus();
}
}
}, 1);
} else {
firstOpen = false;
}
}
<Select
onMenuOpen={handleMenuOpen.bind(this)}
onMenuClose={handleMenuClose.bind(this)}
menuIsOpen={menuIsOpen}
I have to use the timeout to get outside the react lifecycle and I have to add an additional flag since losing focus on the select ponent will also close it. Like I said that has drawbacks: - a little flicker on the selected text during the focus switch - the dropdown box in select has the wrong coloring (not focused obviously) - switching to another dropdown (like alignment) won't close the other since that already has no focus:
Additional info:
We have to work with slate and slate-react
at version 0.47 as higher versions are not supported by slate-html-serializer
which we need. Maybe this has already been solved in a higher version?
So, I have a somewhat working version but I'd much more prefer a solution where slate takes care of the selection "natively" without me having to handle focus. It should be possible I think without the selected text flickering
and off colors.
- Can you create a sandbox that re-produce the issue, that would help addressing your issue. – ROOT Commented May 15, 2020 at 5:05
- @Pete, even the official demo of slateEditor doesn't handle the selection on dropdown. I added in a codeSandbox: codesandbox.io/s/serene-sunset-ilc44?file=/src/App.js – Shubham Khatri Commented May 15, 2020 at 5:34
- btw, which slate-editor library do you use – Shubham Khatri Commented May 15, 2020 at 5:54
- Oh, it's not even officially supported? But how is any user supposed to have a good experience, when the selected text to which they want to apply a change just isnt highlighted any more?! We're using "slate": "0.47.4" and "slate-react": "0.22.4", – Pete Commented May 15, 2020 at 6:55
- Hmm, that' sounds like it's been a headache. You have my sympathies. I'd consider disabling standard text selection (or overriding the css to hide what normally happens) and then putting in your own text select function. That would let you bypass the hoop jumping and give you control of the highlighted text style in a more granular way. This way you could maintain what text is "styled as highlighted" in react state. Something like this perhaps but modified toward react? : stackoverflow./a/987376/5544119 – SethGoodluck Commented May 18, 2020 at 6:22
1 Answer
Reset to default 5 +250Slate doesn't hold the selection when you focus out of the editor due to a dropdown being opened. Now with buttons it is different as it reapplies the selection
Since you now have to manually apply and get selections a way to do this is to store editor selection in state when you are trying to open the menu from select. When menu is open, reapply selection using Transforms.setSelection
and get the fontSize which you can store in state again to show the focussed value in dropdown
Now once you apply the change, you need to apply the selection again
You can follow the concept used in this PR
const [selection, setSelection] = useState();
const [menuIsOpen, setMenuIsOpen] = useState(false);
const [fontSize, setFontSize] = useState(plugins.detectFontSize());
const handleFontSizeChange = ({value}) => {
plugin.reapplyFontSize({value: rendererFontSize(value)});
handleMenuClose();
};
}
const handleMenuOpen = (editor) => {
setSelection(editor.selection);
setMenuIsOpen(true);
Transforms.setSelection() // pass in the required params here
setFontSize(plugins.detectFontSize());
}
const handleMenuClose = (editor) => {
setMenuIsOpen(false);
Transforms.setSelection() // pass in the required params here based on selection state
}
<Select
onMenuOpen={handleMenuOpen.bind(this)}
onMenuClose={handleMenuClose.bind(this)}
menuIsOpen={menuIsOpen}
value={fontSize}
options={options}
/>
Also have a look at this github issue regarding focus and selection