Slate: prevent focusing void inline nodes

Created on 27 Apr 2017  路  6Comments  路  Source: ianstormtaylor/slate

is it possible to prevent focusing void inline nodes and make them act like characters? i.e. only allow the cursor the be on either side of the node, not "on" it.

I tried to create a rule for this, but couldn't quite figure out how to check if a node has focus:

{
  match: node => node.kind === 'inline' && node.isVoid,
  validate: node => // how to check if the node has focus here?,
  normalize: (transform, node) => // move cursor to either side
}
question

Most helpful comment

for anyone interested, managed to solve this eventually with a plugin like this:

class SkipVoidNodesPlugin {
  direction = '';
  onKeyDown(event, keyData, state) {
    if (keyData.key === 'left') {
      this.direction = 'Previous';
    } else if (keyData.key === 'right') {
      this.direction = 'Next';
    } else {
      this.direction = '';
    }
  }
  onBeforeChange(state) {
    if (this.direction !== '') {
      const focusedNode = state.focusInline || state.focusText;
      if (focusedNode.isVoid) {
        return state.transform()[`collapseToEndOf${this.direction}Text`]().apply();
      }
    }
  }
}

All 6 comments

The selection and the nodes are two distinct data sets. The schema rules can't be used to "normalize" the selection.

Instead you could apply a transform in your onChange if the selection is in a void node:

class App extends React.Component {
    state = {
        state: initialState
    }

    onChange = (state) => {
        const { startBlock, document } = state;

        // The selection is in a void node.
        if (startBlock.isVoid) {
            const next = document.getNextBlock(startBlock.key);

            // The transform can be improved to move to next OR prev block
            // By comparing the selection with the one of this.state.state
            // And testing that "next" exists
            state = state.transform()
                .collapseToStartOf(nextBlock)
                .apply()
        }

        this.setState({ state })
    }

    render = () => {
        return (
            <Editor
                state={this.state.state}
                onChange={this.onChange}
                />
        )
    }
}

This example can be improved, and maybe @ianstormtaylor has a better/simpler solution :)

Note: it could be exported as a Slate plugin using onBeforeChange:

const noSelectionInVoid = {
    onBeforeChange(state) {
       const { startBlock, document } = state

        // The selection is in a void node.
        if (startBlock.isVoid) {
            const next = document.getNextBlock(startBlock.key);

            // The transform can be improved to move to next OR prev block
            // By comparing the selection with the one of this.state.state
            // And testing that "next" exists
            state = state.transform()
                .collapseToStartOf(nextBlock)
                .apply()
        }

        return state
    }
};

and you can pass this plugin to your <Editor plugins={[ noSelectionInVoid ]} />

@SamyPesse I see, think I'll go with the plugin approach then :)

one more thing: how would I get this.state.state for comparing previous cursor position in the plugin example?

I don't think it's possible to read the current state in onBeforeChange in a plugin

for anyone interested, managed to solve this eventually with a plugin like this:

class SkipVoidNodesPlugin {
  direction = '';
  onKeyDown(event, keyData, state) {
    if (keyData.key === 'left') {
      this.direction = 'Previous';
    } else if (keyData.key === 'right') {
      this.direction = 'Next';
    } else {
      this.direction = '';
    }
  }
  onBeforeChange(state) {
    if (this.direction !== '') {
      const focusedNode = state.focusInline || state.focusText;
      if (focusedNode.isVoid) {
        return state.transform()[`collapseToEndOf${this.direction}Text`]().apply();
      }
    }
  }
}

for those looking at this today, the bad news is that onBeforeChange, Transform and State have gone. the good news seems to be that the same trick works if you do it in onChange with Change, with something like this:

    onChange = (change: Change) => {
        let value = change.value;
        if (this.direction !== '') {
            const focusedNode = value.focusInline || value.focusText;
            if (focusedNode.isVoid) {
                change = change[`collapseToEndOf${ this.direction }Text`]();
                value = change.value;
            }
        }
        // ...
        this.setState({
            value,
        });
    }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

YurkaninRyan picture YurkaninRyan  路  3Comments

chrpeter picture chrpeter  路  3Comments

JSH3R0 picture JSH3R0  路  3Comments

bengotow picture bengotow  路  3Comments

bunterWolf picture bunterWolf  路  3Comments