Handling tab for lists in Draft.js - draftjs

I have a wrapper around the Editor provided by Draft.js, and I would like to get the tab/shift-tab keys working like they should for the UL and OL. I have the following methods defined:
_onChange(editorState) {
this.setState({editorState});
if (this.props.onChange) {
this.props.onChange(
new CustomEvent('chimpeditor_update',
{
detail: stateToHTML(editorState.getCurrentContent())
})
);
}
}
_onTab(event) {
console.log('onTab');
this._onChange(RichUtils.onTab(event, this.state.editorState, 6));
}
Here I have a method, _onTab, which is connected to the Editor.onTab, where I call RichUtil.onTab(), which I assume returns the updated EditorState, which I then pass to a generic method that updates the EditorState and calls some callbacks. But, when I hit tab or shift-tab, nothing happens at all.

So this came up while implementing with React Hooks, and a google search had this answer as the #2 result.
I believe the code OP has is correct, and I was seeing "nothing happening" as well. The problem turned out to be not including the Draft.css styles.
import 'draft-js/dist/Draft.css'

import { Editor, RichUtils, getDefaultKeyBinding } from 'draft-js'
handleEditorChange = editorState => this.setState({ editorState })
handleKeyBindings = e => {
const { editorState } = this.state
if (e.keyCode === 9) {
const newEditorState = RichUtils.onTab(e, editorState, 6 /* maxDepth */)
if (newEditorState !== editorState) {
this.handleEditorChange(newEditorState)
}
return
}
return getDefaultKeyBinding(e)
}
render() {
return <Editor onTab={this.handleKeyBindings} />
}

The following example will inject \t into the current location, and update the state accordingly.
function custKeyBindingFn(event) {
if (event.keyCode === 9) {
let newContentState = Modifier.replaceText(
editorState.getCurrentContent(),
editorState.getSelection(),
'\t'
);
setEditorState(EditorState.push(editorState, newContentState, 'insert-characters'));
event.preventDefault(); // For good measure. (?)
return null;
}
return getDefaultKeyBinding(event);
}

Related

Draft-JS - Entity component doesn't re-render when data changes

I would like to have my text editor display how many times a content block has been selected and executed though a key command.
I'm doing this by applying an entity to the selected block with a evaluatedTimes property.
The data is changed correctly but the entity component doesn't re-render until I insert new characters in the block of text.
The decorator strategy of the entity doesn't get called, so the only way out I found is to update the editor state with a new decorator instance. This way the entity components get re-rendered but the solution fills a bit hacky.
dependencies:
"draft-js": "^0.11.3",
"draft-js-plugins-editor": "^3.0.0",
"draftjs-utils": "^0.10.2"
code:
// plugin.js
import { EditorState } from "draft-js";
import { getSelectionText, getSelectionEntity } from "draftjs-utils";
import { EvaluatedSpan } from "./components";
import { findEvaluatedEntities, createEvaluatedEntity } from "./entities";
export function createCodeEvaluationPlugin({ onEvaluate = () => {} }) {
return {
decorators: [
{
strategy: findEvaluatedEntities,
component: EvaluatedSpan
}
],
keyBindingFn: e => {
// CMD + ENTER
if (e.metaKey && e.keyCode === 13) {
return "evaluate";
}
},
handleKeyCommand: (command, editorState, _, { setEditorState }) => {
if (command === "evaluate") {
const selectionState = editorState.getSelection();
const contentState = editorState.getCurrentContent();
const entityKey = getSelectionEntity(editorState);
// If selection contains an entity:
if (!entityKey) {
// Create new entity and update editor.
setEditorState(createEvaluatedEntity(editorState, selectionState));
} else {
// Modify entity data.
const entity = contentState.getEntity(entityKey);
const nextContentState = contentState.mergeEntityData(entityKey, {
evaluatedTimes: entity.data.evaluatedTimes + 1
});
// Update editor.
setEditorState(
EditorState.push(editorState, nextContentState, "change-block-data")
);
}
// Pass text to callback handle
const selectionText = getSelectionText(editorState);
onEvaluate(selectionText);
return "handled";
}
return "not-handled";
}
};
}
According to this (https://github.com/facebook/draft-js/issues/1702) you could also update the selection state, which may be sufficient for you.
Works for me:
function forceRerender(editorState: EditorState): EditorState {
const selectionState = editorState.getSelection();
return EditorState.forceSelection(editorState, selectionState);
}

Why TextDocumentContentProvider dont call provideTextDocumentContent on update when query params changes?

as title says, when i wanna update TextDocumentContentProvider with different query params by calling update method provideTextDocumentContent is not called...
only way i managed to get it working was with same URI as in calling
vscode.commands.executeCommand('vscode.previewHtml', URI, 2, 'Storybook');
relevant part of code:
// calculates uri based on editor state - depends on actual caret position
// all uris will start with 'storybook://preview'
function getPreviewUri(editor: vscode.TextEditor): vscode.Uri;
// transforms uri, so web server will understand
// ex: 'storybook://preview?name=fred' -> 'http://localhost:12345/preview/fred?full=1'
function transformUri(uri: vscode.Uri): vscode.Uri;
class StorybookContentProvider implements vscode.TextDocumentContentProvider
{
provideTextDocumentContent(uri: vscode.Uri): string {
var httpUri = transformUri(uri);
return `<iframe src="${httpUri}" />`;
}
onDidChange = new vscode.EventEmitter<vscode.Uri>();
update(uri: vscode.Uri) {
this.onDidChange(uri);
}
}
export function activate(context: vscode.ExtensionContext)
{
vscode.workspace.onDidChangeTextDocument(
(e: vscode.TextDocumentChangeEvent) => {
if (e.document === vscode.window.activeTextEditor.document) {
const previewUri = getPreviewUri(vscode.window.activeTextEditor);
provider.update(previewUri);
}
}
);
vscode.window.onDidChangeTextEditorSelection(
(e: vscode.TextEditorSelectionChangeEvent) => {
if (e.textEditor === vscode.window.activeTextEditor) {
const previewUri = getPreviewUri(vscode.window.activeTextEditor);
provider.update(previewUri);
}
}
);
const provider = new StorybookContentProvider();
context.subscriptions.push(
vscode.commands.registerCommand('extension.showStorybook', () => {
vscode.commands.executeCommand('vscode.previewHtml', vscode.Uri.parse('storybook://preview'), 2, 'Storybook')
}),
vscode.workspace.registerTextDocumentContentProvider('storybook', provider)
);
}

How to provide plugin for all forms in redux-form?

Ok, so I wanted to deal with whitespaces at the beginning of the input in my register form and I have achieved this by providing plugin for redux-form reducer:
export default function(cookies, server) {
return combineReducers({
auth: auth(cookies, server),
reduxAsyncConnect,
alert,
programs,
exercises,
routing: routerReducer,
form: form.plugin(formPlugin),
profile,
spinner,
companies
});
}
The plugin is:
import {ltrim} from './ltrim';
const formPlugin = {
registerForm: (state, action) => {
switch(action.type) {
case '##redux-form/CHANGE':
if (action.meta.form === 'registerForm') {
return {
...state,
values: {
...state.values,
[action.meta.field]: ltrim(action.payload)
}
}
} else {
return state;
}
default:
return state;
}
}
}
export default formPlugin;
How can I get the same effect on all forms withouth hardcoding? Maybe I have to somehow edit redux-form CHANGE action to achieve this?
You could make a custom Field component that does this for you.
import { Field } from 'redux-form';
import {ltrim} from './ltrim';
const normalize = value => ltrim(value);
const TrimmedField = props => <Field {...props} normalize={normalize} />;
You could also give your custom normalize a callback argument if you need to do other normalization in some cases.
http://redux-form.com/6.6.3/docs/api/Field.md/#-normalize-value-previousvalue-allvalues-previousallvalues-nextvalue-optional-

React input defaultValue focus and select

I have a component with an input-element, i use defaultValue to set the initial value.
I want to focus that element and select the whole value initially, but it seems that the defaultvalue is not set when componentDidMount is called.
Do you have any tips?
i use window.setTimeout but i want to avoid that in my react-components:
public componentDidMount(): void {
if (this.props.focus) {
let tInput: HTMLInputElement = ReactDOM.findDOMNode(this).getElementsByTagName("input").item(0);
if (tInput) {
tInput.focus();
// FixMe: defaultValue is set too late by react so i cant set selection instantly
if (this.props.defaultValue) {
window.setTimeout(
() => {tInput.setSelectionRange(0, this.props.defaultValue.length); },
100
);
}
}
}
}
Works fine for me:
class MyComponent extends React.Component {
componentDidMount () {
const { myInput } = this.refs
myInput.focus()
myInput.select()
}
render () {
return (
<input type='text' defaultValue='Foobar' ref='myInput' />
)
}
}
I wouldn't use any reactDOM methods other than render/renderToString. These apis are "escape hatches" and usage is not recommended.

How to use node-simple-schema reactively?

Given that there is not much examples about this, I am following the docs as best as I can, but the validation is not reactive.
I declare a schema :
import { Tracker } from 'meteor/tracker';
import SimpleSchema from 'simpl-schema';
export const modelSchema = new SimpleSchema({
foo: {
type: String,
custom() {
setTimeout(() => {
this.addValidationErrors([{ name: 'foo', type: 'notUnique' }]);
}, 100); // simulate async
return false;
}
}
}, {
tracker: Tracker
});
then I use this schema in my component :
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.newContext();
this.state = {
isValid: this.validation.isValid()
};
}
...
render() {
...
const errors = this.validation._validationErrors;
return (
...
)
}
}
So, whenever I try to validate foo, the asynchronous' custom function is called, and the proper addValidationErrors function is called, but the component is never re-rendered when this.validation.isValid() is supposed to be false.
What am I missing?
There are actually two errors in your code. Firstly this.addValidationErrors cannot be used asynchronously inside custom validation, as it does not refer to the correct validation context. Secondly, TrackerReact only registers reactive data sources (such as .isValid) inside the render function, so it's not sufficient to only access _validationErrors in it. Thus to get it working you need to use a named validation context, and call isValid in the render function (or some other function called by it) like this:
in the validation
custom() {
setTimeout(() => {
modelSchema.namedContext().addValidationErrors([
{ name: 'foo', type: 'notUnique' }
]);
}, 100);
}
the component
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.namedContext();
}
render() {
let errors = [];
if (!this.validation.isValid()) {
errors = this.validation._validationErrors;
}
return (
...
)
}
}
See more about asynchronous validation here.