mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-18 21:21:21 +02:00
* Add base tests for isVoid with around text node * Ensure that void nodes are surrounded by text nodes in Node.normalize * Only wrap inline void nodes with text * Fix indentation * Add emojis example * Adapt unit test "transforms/fixtures/at-current-range/set-inline/with-is-void" * Adapt unit test "transforms/fixtures/at-current-range/insert-inline/with-inline" * Adapt unit test "transforms/fixtures/at-current-range/insert-inline/block-start" * add passing parent to leaf nodes, for rendering breaks * add zero-width spaces in empty text nodes, to allow selections * add zero-width space handling to copy/cut * fix delete handling around inline void nodes * fix tests for inline void nodes * fix style * fix void cursor handling across browsers * fix void rendering tests
This commit is contained in:
committed by
Ian Storm Taylor
parent
f7e132ed61
commit
d1c3700bd2
8
examples/emojis/Readme.md
Normal file
8
examples/emojis/Readme.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
# Emojis Example
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This example shows you how you can insert inline void nodes. This is how you'd add emojis or inline images to Slate.
|
||||||
|
|
||||||
|
Check out the [Examples readme](..) to see how to run it!
|
144
examples/emojis/index.js
Normal file
144
examples/emojis/index.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
|
||||||
|
import { Editor, Mark, Raw } from '../..'
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import initialState from './state.json'
|
||||||
|
import isUrl from 'is-url'
|
||||||
|
import { Map } from 'immutable'
|
||||||
|
|
||||||
|
|
||||||
|
const EMOJIS = [
|
||||||
|
'😃', '😬', '🍔'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a schema.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
nodes: {
|
||||||
|
paragraph: props => <p>{props.children}</p>,
|
||||||
|
emoji: (props) => {
|
||||||
|
const { state, node } = props
|
||||||
|
const { data } = node
|
||||||
|
const code = data.get('code')
|
||||||
|
const isSelected = state.selection.hasFocusIn(node)
|
||||||
|
return <span className={`emoji ${isSelected ? 'selected' : ''}`} {...props.attributes} contentEditable={false}>{code}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The links example.
|
||||||
|
*
|
||||||
|
* @type {Component}
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Emojis extends React.Component {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the raw initial state.
|
||||||
|
*
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
|
||||||
|
state = {
|
||||||
|
state: Raw.deserialize(initialState, { terse: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On change.
|
||||||
|
*
|
||||||
|
* @param {State} state
|
||||||
|
*/
|
||||||
|
|
||||||
|
onChange = (state) => {
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When clicking a emoji, insert it
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
|
|
||||||
|
onClickEmoji = (e, code) => {
|
||||||
|
e.preventDefault()
|
||||||
|
let { state } = this.state
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.transform()
|
||||||
|
.insertInline({
|
||||||
|
type: 'emoji',
|
||||||
|
isVoid: true,
|
||||||
|
data: { code }
|
||||||
|
})
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
this.setState({ state })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the app.
|
||||||
|
*
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
render = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{this.renderToolbar()}
|
||||||
|
{this.renderEditor()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the toolbar.
|
||||||
|
*
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderToolbar = () => {
|
||||||
|
return (
|
||||||
|
<div className="menu toolbar-menu">
|
||||||
|
{EMOJIS.map((emoji, i) => {
|
||||||
|
const onMouseDown = e => this.onClickEmoji(e, emoji)
|
||||||
|
return (
|
||||||
|
<span key={i} className="button" onMouseDown={onMouseDown}>
|
||||||
|
<span className="material-icons">{emoji}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the editor.
|
||||||
|
*
|
||||||
|
* @return {Element} element
|
||||||
|
*/
|
||||||
|
|
||||||
|
renderEditor = () => {
|
||||||
|
return (
|
||||||
|
<div className="editor">
|
||||||
|
<Editor
|
||||||
|
schema={schema}
|
||||||
|
state={this.state.state}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default Emojis
|
50
examples/emojis/state.json
Normal file
50
examples/emojis/state.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"text": "In addition to block nodes, you can create inline void nodes, like "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "inline",
|
||||||
|
"type": "emoji",
|
||||||
|
"isVoid": true,
|
||||||
|
"data": {
|
||||||
|
"code": "😃"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"text": "!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "inline",
|
||||||
|
"type": "emoji",
|
||||||
|
"isVoid": true,
|
||||||
|
"data": {
|
||||||
|
"code": "🍔"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "block",
|
||||||
|
"type": "paragraph",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"kind": "text",
|
||||||
|
"text": "This example shows emojis in action."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -165,3 +165,7 @@ input:focus {
|
|||||||
.hover-menu .button[data-active="true"] {
|
.hover-menu .button[data-active="true"] {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji.selected {
|
||||||
|
outline: 2px solid blue;
|
||||||
|
}
|
||||||
|
@@ -10,6 +10,7 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
|
|||||||
import AutoMarkdown from './auto-markdown'
|
import AutoMarkdown from './auto-markdown'
|
||||||
import CodeHighlighting from './code-highlighting'
|
import CodeHighlighting from './code-highlighting'
|
||||||
import Embeds from './embeds'
|
import Embeds from './embeds'
|
||||||
|
import Emojis from './emojis'
|
||||||
import HoveringMenu from './hovering-menu'
|
import HoveringMenu from './hovering-menu'
|
||||||
import Iframes from './iframes'
|
import Iframes from './iframes'
|
||||||
import Images from './images'
|
import Images from './images'
|
||||||
@@ -72,6 +73,7 @@ class App extends React.Component {
|
|||||||
{this.renderTab('Links', 'links')}
|
{this.renderTab('Links', 'links')}
|
||||||
{this.renderTab('Images', 'images')}
|
{this.renderTab('Images', 'images')}
|
||||||
{this.renderTab('Embeds', 'embeds')}
|
{this.renderTab('Embeds', 'embeds')}
|
||||||
|
{this.renderTab('Emojis', 'emojis')}
|
||||||
{this.renderTab('Tables', 'tables')}
|
{this.renderTab('Tables', 'tables')}
|
||||||
{this.renderTab('Code Highlighting', 'code-highlighting')}
|
{this.renderTab('Code Highlighting', 'code-highlighting')}
|
||||||
{this.renderTab('Paste HTML', 'paste-html')}
|
{this.renderTab('Paste HTML', 'paste-html')}
|
||||||
@@ -126,6 +128,7 @@ const router = (
|
|||||||
<Route path="auto-markdown" component={AutoMarkdown} />
|
<Route path="auto-markdown" component={AutoMarkdown} />
|
||||||
<Route path="code-highlighting" component={CodeHighlighting} />
|
<Route path="code-highlighting" component={CodeHighlighting} />
|
||||||
<Route path="embeds" component={Embeds} />
|
<Route path="embeds" component={Embeds} />
|
||||||
|
<Route path="emojis" component={Emojis} />
|
||||||
<Route path="hovering-menu" component={HoveringMenu} />
|
<Route path="hovering-menu" component={HoveringMenu} />
|
||||||
<Route path="iframes" component={Iframes} />
|
<Route path="iframes" component={Iframes} />
|
||||||
<Route path="images" component={Images} />
|
<Route path="images" component={Images} />
|
||||||
|
@@ -691,6 +691,7 @@ class Content extends React.Component {
|
|||||||
<Node
|
<Node
|
||||||
key={node.key}
|
key={node.key}
|
||||||
node={node}
|
node={node}
|
||||||
|
parent={state.document}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
state={state}
|
state={state}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
|
@@ -32,6 +32,7 @@ class Leaf extends React.Component {
|
|||||||
isVoid: React.PropTypes.bool,
|
isVoid: React.PropTypes.bool,
|
||||||
marks: React.PropTypes.object.isRequired,
|
marks: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
parent: React.PropTypes.object.isRequired,
|
||||||
ranges: React.PropTypes.object.isRequired,
|
ranges: React.PropTypes.object.isRequired,
|
||||||
schema: React.PropTypes.object.isRequired,
|
schema: React.PropTypes.object.isRequired,
|
||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
@@ -243,10 +244,15 @@ class Leaf extends React.Component {
|
|||||||
* @return {Element}
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
renderText({ text, index, ranges }) {
|
renderText({ parent, text, index, ranges }) {
|
||||||
// If the text is empty, we need to render a <br/> to get the block to have
|
// COMPAT: If the text is empty and it's the only child, we need to render a
|
||||||
// the proper height.
|
// <br/> to get the block to have the proper height.
|
||||||
if (text == '') return <br />
|
if (text == '' && parent.kind == 'block' && parent.text == '') return <br />
|
||||||
|
|
||||||
|
// COMPAT: If the text is empty otherwise, it's because it's on the edge of
|
||||||
|
// an inline void node, so we render a zero-width space so that the
|
||||||
|
// selection can be inserted next to it still.
|
||||||
|
if (text == '') return <span className="slate-zero-width-space">{'\u200B'}</span>
|
||||||
|
|
||||||
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
|
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
|
||||||
// so we need to add an extra trailing new lines to prevent that.
|
// so we need to add an extra trailing new lines to prevent that.
|
||||||
|
@@ -33,6 +33,7 @@ class Node extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
editor: React.PropTypes.object.isRequired,
|
editor: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
parent: React.PropTypes.object.isRequired,
|
||||||
schema: React.PropTypes.object.isRequired,
|
schema: React.PropTypes.object.isRequired,
|
||||||
state: React.PropTypes.object.isRequired
|
state: React.PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
@@ -237,6 +238,7 @@ class Node extends React.Component {
|
|||||||
<Node
|
<Node
|
||||||
key={child.key}
|
key={child.key}
|
||||||
node={child}
|
node={child}
|
||||||
|
parent={this.props.node}
|
||||||
editor={this.props.editor}
|
editor={this.props.editor}
|
||||||
schema={this.props.schema}
|
schema={this.props.schema}
|
||||||
state={this.props.state}
|
state={this.props.state}
|
||||||
@@ -251,7 +253,7 @@ class Node extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderElement = () => {
|
renderElement = () => {
|
||||||
const { editor, node, state } = this.props
|
const { editor, node, parent, state } = this.props
|
||||||
const { Component } = this.state
|
const { Component } = this.state
|
||||||
const children = node.nodes
|
const children = node.nodes
|
||||||
.map(child => this.renderNode(child))
|
.map(child => this.renderNode(child))
|
||||||
@@ -276,6 +278,7 @@ class Node extends React.Component {
|
|||||||
attributes={attributes}
|
attributes={attributes}
|
||||||
key={node.key}
|
key={node.key}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
|
parent={parent}
|
||||||
node={node}
|
node={node}
|
||||||
state={state}
|
state={state}
|
||||||
>
|
>
|
||||||
@@ -325,7 +328,7 @@ class Node extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderLeaf = (ranges, range, index, offset) => {
|
renderLeaf = (ranges, range, index, offset) => {
|
||||||
const { node, schema, state } = this.props
|
const { node, parent, schema, state } = this.props
|
||||||
const text = range.text
|
const text = range.text
|
||||||
const marks = range.marks
|
const marks = range.marks
|
||||||
|
|
||||||
@@ -335,6 +338,7 @@ class Node extends React.Component {
|
|||||||
index={index}
|
index={index}
|
||||||
marks={marks}
|
marks={marks}
|
||||||
node={node}
|
node={node}
|
||||||
|
parent={parent}
|
||||||
ranges={ranges}
|
ranges={ranges}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
state={state}
|
state={state}
|
||||||
|
@@ -31,6 +31,7 @@ class Void extends React.Component {
|
|||||||
children: React.PropTypes.any.isRequired,
|
children: React.PropTypes.any.isRequired,
|
||||||
editor: React.PropTypes.object.isRequired,
|
editor: React.PropTypes.object.isRequired,
|
||||||
node: React.PropTypes.object.isRequired,
|
node: React.PropTypes.object.isRequired,
|
||||||
|
parent: React.PropTypes.object.isRequired,
|
||||||
schema: React.PropTypes.object.isRequired,
|
schema: React.PropTypes.object.isRequired,
|
||||||
state: React.PropTypes.object.isRequired,
|
state: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
@@ -66,7 +67,8 @@ class Void extends React.Component {
|
|||||||
|
|
||||||
// Make the outer wrapper relative, so the spacer can overlay it.
|
// Make the outer wrapper relative, so the spacer can overlay it.
|
||||||
const style = {
|
const style = {
|
||||||
position: 'relative'
|
position: 'relative',
|
||||||
|
lineHeight: '0px'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,21 +91,11 @@ class Void extends React.Component {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
renderSpacer = () => {
|
renderSpacer = () => {
|
||||||
// COMPAT: In Firefox, if the <span> is positioned absolutely, it won't
|
const style = {
|
||||||
// receive the cursor properly when navigating via arrow keys.
|
position: 'relative',
|
||||||
const style = IS_FIREFOX
|
|
||||||
? {
|
|
||||||
pointerEvents: 'none',
|
|
||||||
width: '0px',
|
|
||||||
height: '0px',
|
|
||||||
lineHeight: '0px',
|
|
||||||
visibility: 'hidden'
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '0px',
|
top: '0px',
|
||||||
left: '-9999px',
|
left: '-9999px',
|
||||||
textIndent: '-9999px'
|
textIndent: '-9999px',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -137,6 +129,7 @@ class Void extends React.Component {
|
|||||||
schema={schema}
|
schema={schema}
|
||||||
state={state}
|
state={state}
|
||||||
node={child}
|
node={child}
|
||||||
|
parent={node}
|
||||||
ranges={ranges}
|
ranges={ranges}
|
||||||
index={index}
|
index={index}
|
||||||
text={text}
|
text={text}
|
||||||
|
@@ -1121,6 +1121,37 @@ const Node = {
|
|||||||
node = node.removeDescendant(key)
|
node = node.removeDescendant(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ensure that void nodes are surrounded by text nodes
|
||||||
|
node = node.mapDescendants((desc) => {
|
||||||
|
if (desc.kind == 'text') {
|
||||||
|
return desc
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = desc.nodes.reduce((accu, child, i) => {
|
||||||
|
// We wrap only inline void nodes
|
||||||
|
if (!child.isVoid || child.kind === 'block') {
|
||||||
|
return accu.push(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev = accu.last()
|
||||||
|
const next = desc.nodes.get(i + 1)
|
||||||
|
|
||||||
|
if (!prev || prev.kind !== 'text') {
|
||||||
|
accu = accu.push(Text.create())
|
||||||
|
}
|
||||||
|
|
||||||
|
accu = accu.push(child)
|
||||||
|
|
||||||
|
if (!next || next.kind !== 'text') {
|
||||||
|
accu = accu.push(Text.create())
|
||||||
|
}
|
||||||
|
|
||||||
|
return accu
|
||||||
|
}, List())
|
||||||
|
|
||||||
|
return desc.merge({ nodes })
|
||||||
|
})
|
||||||
|
|
||||||
return node
|
return node
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -188,11 +188,16 @@ function Plugin(options = {}) {
|
|||||||
|
|
||||||
const { fragment } = data
|
const { fragment } = data
|
||||||
const encoded = Base64.serializeNode(fragment)
|
const encoded = Base64.serializeNode(fragment)
|
||||||
|
const range = native.getRangeAt(0)
|
||||||
|
const contents = range.cloneContents()
|
||||||
|
|
||||||
|
// Remove any zero-width space spans from the cloned DOM so that they don't
|
||||||
|
// show up elsewhere when copied.
|
||||||
|
const zws = [].slice.call(contents.querySelectorAll('.slate-zero-width-space'))
|
||||||
|
zws.forEach(zw => zw.parentNode.removeChild(zw))
|
||||||
|
|
||||||
// Wrap the first character of the selection in a span that has the encoded
|
// Wrap the first character of the selection in a span that has the encoded
|
||||||
// fragment attached as an attribute, so it will show up in the copied HTML.
|
// fragment attached as an attribute, so it will show up in the copied HTML.
|
||||||
const range = native.getRangeAt(0)
|
|
||||||
const contents = range.cloneContents()
|
|
||||||
const wrapper = window.document.createElement('span')
|
const wrapper = window.document.createElement('span')
|
||||||
const text = contents.childNodes[0]
|
const text = contents.childNodes[0]
|
||||||
const char = text.textContent.slice(0, 1)
|
const char = text.textContent.slice(0, 1)
|
||||||
@@ -328,6 +333,8 @@ function Plugin(options = {}) {
|
|||||||
case 'enter': return onKeyDownEnter(e, data, state)
|
case 'enter': return onKeyDownEnter(e, data, state)
|
||||||
case 'backspace': return onKeyDownBackspace(e, data, state)
|
case 'backspace': return onKeyDownBackspace(e, data, state)
|
||||||
case 'delete': return onKeyDownDelete(e, data, state)
|
case 'delete': return onKeyDownDelete(e, data, state)
|
||||||
|
case 'left': return onKeyDownLeft(e, data, state)
|
||||||
|
case 'right': return onKeyDownRight(e, data, state)
|
||||||
case 'y': return onKeyDownY(e, data, state)
|
case 'y': return onKeyDownY(e, data, state)
|
||||||
case 'z': return onKeyDownZ(e, data, state)
|
case 'z': return onKeyDownZ(e, data, state)
|
||||||
}
|
}
|
||||||
@@ -450,6 +457,82 @@ function Plugin(options = {}) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On `left` key down, move backward.
|
||||||
|
*
|
||||||
|
* COMPAT: This is required to solve for the case where an inline void node is
|
||||||
|
* surrounded by empty text nodes with zero-width spaces in them. Without this
|
||||||
|
* the zero-width spaces will cause two arrow keys to jump to the next text.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onKeyDownLeft(e, data, state) {
|
||||||
|
if (data.isCtrl) return
|
||||||
|
if (data.isOpt) return
|
||||||
|
if (state.isExpanded) return
|
||||||
|
|
||||||
|
const { document, startText } = state
|
||||||
|
const hasVoidParent = document.hasVoidParent(startText)
|
||||||
|
|
||||||
|
if (
|
||||||
|
startText.text == '' ||
|
||||||
|
hasVoidParent
|
||||||
|
) {
|
||||||
|
const previousText = document.getPreviousText(startText)
|
||||||
|
if (!previousText) return
|
||||||
|
|
||||||
|
debug('onKeyDownLeft', { data })
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
return state
|
||||||
|
.transform()
|
||||||
|
.collapseToEndOf(previousText)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On `right` key down, move forward.
|
||||||
|
*
|
||||||
|
* COMPAT: This is required to solve for the case where an inline void node is
|
||||||
|
* surrounded by empty text nodes with zero-width spaces in them. Without this
|
||||||
|
* the zero-width spaces will cause two arrow keys to jump to the next text.
|
||||||
|
*
|
||||||
|
* @param {Event} e
|
||||||
|
* @param {Object} data
|
||||||
|
* @param {State} state
|
||||||
|
* @return {State}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onKeyDownRight(e, data, state) {
|
||||||
|
if (data.isCtrl) return
|
||||||
|
if (data.isOpt) return
|
||||||
|
if (state.isExpanded) return
|
||||||
|
|
||||||
|
const { document, startText } = state
|
||||||
|
const hasVoidParent = document.hasVoidParent(startText)
|
||||||
|
|
||||||
|
if (
|
||||||
|
startText.text == '' ||
|
||||||
|
hasVoidParent
|
||||||
|
) {
|
||||||
|
const nextText = document.getNextText(startText)
|
||||||
|
if (!nextText) return state
|
||||||
|
|
||||||
|
debug('onKeyDownRight', { data })
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
return state
|
||||||
|
.transform()
|
||||||
|
.collapseToStartOf(nextText)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On `y` key down, redo.
|
* On `y` key down, redo.
|
||||||
*
|
*
|
||||||
|
@@ -117,7 +117,8 @@ export function deleteBackward(transform, n = 1) {
|
|||||||
if (prevBlock && prevBlock.isVoid) {
|
if (prevBlock && prevBlock.isVoid) {
|
||||||
after = selection
|
after = selection
|
||||||
} else if (prevInline && prevInline.isVoid) {
|
} else if (prevInline && prevInline.isVoid) {
|
||||||
after = selection
|
const prevPrev = document.getPreviousText(previous)
|
||||||
|
after = selection.collapseToEndOf(prevPrev)
|
||||||
} else {
|
} else {
|
||||||
after = selection.collapseToEndOf(previous)
|
after = selection.collapseToEndOf(previous)
|
||||||
}
|
}
|
||||||
@@ -321,7 +322,13 @@ export function insertInline(transform, inline) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
const text = document.getTexts().find(n => !keys.includes(n.key))
|
const text = document.getTexts().find((n) => {
|
||||||
|
if (keys.includes(n.key)) return false
|
||||||
|
const parent = document.getParent(n)
|
||||||
|
if (parent.kind != 'inline') return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
after = selection.collapseToEndOf(text)
|
after = selection.collapseToEndOf(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -195,6 +195,16 @@ export function deleteForwardAtRange(transform, range, n = 1) {
|
|||||||
|
|
||||||
if (range.isAtEndOf(text)) {
|
if (range.isAtEndOf(text)) {
|
||||||
const next = document.getNextText(text)
|
const next = document.getNextText(text)
|
||||||
|
const nextBlock = document.getClosestBlock(next)
|
||||||
|
const nextInline = document.getClosestInline(next)
|
||||||
|
|
||||||
|
if (nextBlock && nextBlock.isVoid) {
|
||||||
|
return transform.removeNodeByKey(nextBlock.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextInline && nextInline.isVoid) {
|
||||||
|
return transform.removeNodeByKey(nextInline.key)
|
||||||
|
}
|
||||||
|
|
||||||
range = range.merge({
|
range = range.merge({
|
||||||
focusKey: next.key,
|
focusKey: next.key,
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
|
|
||||||
<div contenteditable="true">
|
<div contenteditable="true">
|
||||||
<div style="position:relative;">
|
<div style="position:relative;line-height:0px;">
|
||||||
<span style="position:absolute;top:0px;left:-9999px;text-indent:-9999px;">
|
<span style="position:relative;top:0px;left:-9999px;text-indent:-9999px;">
|
||||||
<span>
|
<span>
|
||||||
<br>
|
<span class="slate-zero-width-space">​</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div contenteditable="false">
|
<div contenteditable="false">
|
||||||
|
12
test/rendering/fixtures/custom-inline-void/index.js
Normal file
12
test/rendering/fixtures/custom-inline-void/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
function Image(props) {
|
||||||
|
return <img {...props.attributes} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const schema = {
|
||||||
|
nodes: {
|
||||||
|
image: Image
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,8 @@
|
|||||||
|
|
||||||
nodes:
|
nodes:
|
||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: default
|
||||||
nodes:
|
nodes:
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: image
|
type: image
|
||||||
isVoid: true
|
isVoid: true
|
||||||
- kind: text
|
|
||||||
text: word
|
|
25
test/rendering/fixtures/custom-inline-void/output.html
Normal file
25
test/rendering/fixtures/custom-inline-void/output.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
<div contenteditable="true">
|
||||||
|
<div style="position:relative;">
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
<span class="slate-zero-width-space">​</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span style="position:relative;line-height:0px;">
|
||||||
|
<span style="position:relative;top:0px;left:-9999px;text-indent:-9999px;">
|
||||||
|
<span>
|
||||||
|
<span class="slate-zero-width-space">​</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span contenteditable="false">
|
||||||
|
<img>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<span>
|
||||||
|
<span class="slate-zero-width-space">​</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
2
test/schema/fixtures/default-void-text-around/index.js
Normal file
2
test/schema/fixtures/default-void-text-around/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
export default {}
|
8
test/schema/fixtures/default-void-text-around/input.yaml
Normal file
8
test/schema/fixtures/default-void-text-around/input.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: default
|
||||||
|
nodes:
|
||||||
|
- kind: inline
|
||||||
|
isVoid: true
|
||||||
|
type: image
|
13
test/schema/fixtures/default-void-text-around/output.yaml
Normal file
13
test/schema/fixtures/default-void-text-around/output.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
- kind: block
|
||||||
|
type: default
|
||||||
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
- kind: inline
|
||||||
|
isVoid: true
|
||||||
|
type: image
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
@@ -4,6 +4,7 @@ nodes:
|
|||||||
isVoid: false
|
isVoid: false
|
||||||
data: {}
|
data: {}
|
||||||
nodes:
|
nodes:
|
||||||
|
- characters: []
|
||||||
- type: link
|
- type: link
|
||||||
isVoid: true
|
isVoid: true
|
||||||
data: {}
|
data: {}
|
||||||
@@ -11,3 +12,4 @@ nodes:
|
|||||||
- characters:
|
- characters:
|
||||||
- text: " "
|
- text: " "
|
||||||
marks: []
|
marks: []
|
||||||
|
- characters: []
|
||||||
|
@@ -4,6 +4,7 @@ nodes:
|
|||||||
isVoid: false
|
isVoid: false
|
||||||
data: {}
|
data: {}
|
||||||
nodes:
|
nodes:
|
||||||
|
- characters: []
|
||||||
- type: link
|
- type: link
|
||||||
isVoid: true
|
isVoid: true
|
||||||
data: {}
|
data: {}
|
||||||
@@ -11,3 +12,4 @@ nodes:
|
|||||||
- characters:
|
- characters:
|
||||||
- text: " "
|
- text: " "
|
||||||
marks: []
|
marks: []
|
||||||
|
- characters: []
|
||||||
|
@@ -4,6 +4,7 @@ nodes:
|
|||||||
isVoid: false
|
isVoid: false
|
||||||
data: {}
|
data: {}
|
||||||
nodes:
|
nodes:
|
||||||
|
- characters: []
|
||||||
- type: link
|
- type: link
|
||||||
isVoid: true
|
isVoid: true
|
||||||
data: {}
|
data: {}
|
||||||
@@ -11,3 +12,4 @@ nodes:
|
|||||||
- characters:
|
- characters:
|
||||||
- text: " "
|
- text: " "
|
||||||
marks: []
|
marks: []
|
||||||
|
- characters: []
|
||||||
|
@@ -3,6 +3,10 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: link
|
type: link
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -8,6 +8,11 @@ document:
|
|||||||
data: {}
|
data: {}
|
||||||
isVoid: false
|
isVoid: false
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- kind: range
|
||||||
|
text: ""
|
||||||
|
marks: []
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: link
|
type: link
|
||||||
isVoid: true
|
isVoid: true
|
||||||
@@ -18,3 +23,8 @@ document:
|
|||||||
- kind: range
|
- kind: range
|
||||||
text: " "
|
text: " "
|
||||||
marks: []
|
marks: []
|
||||||
|
- kind: text
|
||||||
|
ranges:
|
||||||
|
- kind: range
|
||||||
|
text: ""
|
||||||
|
marks: []
|
||||||
|
@@ -21,7 +21,7 @@ export default function (state) {
|
|||||||
})
|
})
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
const updated = next.document.getTexts().last()
|
const updated = next.document.getTexts().get(1)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
next.selection.toJS(),
|
next.selection.toJS(),
|
||||||
|
@@ -8,3 +8,5 @@ nodes:
|
|||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -21,7 +21,7 @@ export default function (state) {
|
|||||||
})
|
})
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
const updated = next.document.getTexts().first()
|
const updated = next.document.getTexts().get(1)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
next.selection.toJS(),
|
next.selection.toJS(),
|
||||||
|
@@ -3,6 +3,8 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
@@ -21,7 +21,7 @@ export default function (state) {
|
|||||||
})
|
})
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
const updated = next.document.getTexts().last()
|
const updated = next.document.getTexts().get(1)
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
next.selection.toJS(),
|
next.selection.toJS(),
|
||||||
|
@@ -3,6 +3,10 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
import { Inline } from '../../../../../..'
|
|
||||||
import assert from 'assert'
|
|
||||||
|
|
||||||
export default function (state) {
|
|
||||||
const { document, selection } = state
|
|
||||||
const texts = document.getTexts()
|
|
||||||
const first = texts.first()
|
|
||||||
const range = selection.merge({
|
|
||||||
anchorKey: first.key,
|
|
||||||
anchorOffset: 0,
|
|
||||||
focusKey: first.key,
|
|
||||||
focusOffset: 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const next = state
|
|
||||||
.transform()
|
|
||||||
.moveTo(range)
|
|
||||||
.insertInline(Inline.create({
|
|
||||||
type: 'image',
|
|
||||||
isVoid: true
|
|
||||||
}))
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
const updated = next.document.getTexts().first()
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
next.selection.toJS(),
|
|
||||||
range.collapseToEndOf(updated).toJS()
|
|
||||||
)
|
|
||||||
|
|
||||||
return next
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
nodes:
|
|
||||||
- kind: block
|
|
||||||
type: paragraph
|
|
||||||
nodes:
|
|
||||||
- kind: text
|
|
||||||
text: word
|
|
@@ -3,6 +3,10 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: emoji
|
type: emoji
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -8,3 +8,5 @@ nodes:
|
|||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -3,6 +3,8 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
@@ -3,6 +3,10 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: hashtag
|
type: hashtag
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
@@ -3,6 +3,8 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: image
|
type: image
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
@@ -3,6 +3,10 @@ nodes:
|
|||||||
- kind: block
|
- kind: block
|
||||||
type: paragraph
|
type: paragraph
|
||||||
nodes:
|
nodes:
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
- kind: inline
|
- kind: inline
|
||||||
type: emoji
|
type: emoji
|
||||||
isVoid: true
|
isVoid: true
|
||||||
|
- kind: text
|
||||||
|
text: ""
|
||||||
|
Reference in New Issue
Block a user