diff --git a/examples/embeds/Readme.md b/examples/embeds/Readme.md
new file mode 100644
index 000000000..a1810bcb8
--- /dev/null
+++ b/examples/embeds/Readme.md
@@ -0,0 +1,9 @@
+
+# Images Example
+
+![](../../docs/images/images-example.png)
+
+This example shows you how you can use "void" nodes to render content that has no text in it, like images.
+
+Check out the [Examples readme](..) to see how to run it!
+
diff --git a/examples/embeds/index.js b/examples/embeds/index.js
new file mode 100644
index 000000000..d42cb13bb
--- /dev/null
+++ b/examples/embeds/index.js
@@ -0,0 +1,81 @@
+
+import { Editor, Raw } from '../..'
+import React from 'react'
+import ReactDOM from 'react-dom'
+import Video from './video'
+import initialState from './state.json'
+
+/**
+ * Define a set of node renderers.
+ *
+ * @type {Object}
+ */
+
+const NODES = {
+ video: Video
+}
+
+/**
+ * The images example.
+ *
+ * @type {Component}
+ */
+
+class Embeds 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 })
+ }
+
+ /**
+ * Render the app.
+ *
+ * @return {Element} element
+ */
+
+ render = () => {
+ return (
+
+
+
+ )
+ }
+
+ /**
+ * Render a `node`.
+ *
+ * @param {Node} node
+ * @return {Element}
+ */
+
+ renderNode = (node) => {
+ return NODES[node.type]
+ }
+
+}
+
+/**
+ * Export.
+ */
+
+export default Embeds
diff --git a/examples/embeds/state.json b/examples/embeds/state.json
new file mode 100644
index 000000000..160240a59
--- /dev/null
+++ b/examples/embeds/state.json
@@ -0,0 +1,32 @@
+{
+ "nodes": [
+ {
+ "kind": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "kind": "text",
+ "text": "In addition to simple image nodes, you can actually create complex embedded nodes. For example, this one contains an input element that lets you change the video being rendered!"
+ }
+ ]
+ },
+ {
+ "kind": "block",
+ "type": "video",
+ "isVoid": true,
+ "data": {
+ "video": "https://www.youtube.com/embed/FaHEusBG20c"
+ }
+ },
+ {
+ "kind": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "kind": "text",
+ "text": "Try it out! If you want another good video URL to try, go with: https://www.youtube.com/embed/6Ejga4kJUts"
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/embeds/video.js b/examples/embeds/video.js
new file mode 100644
index 000000000..ceeeae1ec
--- /dev/null
+++ b/examples/embeds/video.js
@@ -0,0 +1,122 @@
+
+import React from 'react'
+import { Void } from '../..'
+
+/**
+ * An video embed component.
+ *
+ * @type {Component}
+ */
+
+class Video extends React.Component {
+
+ /**
+ * When the input text changes, update the `video` data on the node.
+ *
+ * @param {Event} e
+ */
+
+ onChange = (e) => {
+ const video = e.target.value
+ const { node, state, editor } = this.props
+ const properties = {
+ data: { video }
+ }
+
+ const next = state
+ .transform()
+ .setNodeByKey(node.key, properties)
+ .apply()
+
+ editor.onChange(next)
+ }
+
+ /**
+ * When clicks happen in the input, stop propagation so that the void node
+ * itself isn't focused, since that would unfocus the input.
+ *
+ * @type {Event} e
+ */
+
+ onClick = (e) => {
+ e.stopPropagation()
+ }
+
+ /**
+ * Render.
+ *
+ * @return {Element}
+ */
+
+ render = () => {
+ return (
+
+ {this.renderVideo()}
+ {this.renderInput()}
+
+ )
+ }
+
+ /**
+ * Render the Youtube iframe, responsively.
+ *
+ * @return {Element}
+ */
+
+ renderVideo = () => {
+ const video = this.props.node.data.get('video')
+ const wrapperStyle = {
+ position: 'relative',
+ paddingBottom: '66.66%',
+ paddingTop: '25px',
+ height: '0'
+ }
+
+ const iframeStyle = {
+ position: 'absolute',
+ top: '0px',
+ left: '0px',
+ width: '100%',
+ height: '100%'
+ }
+
+ return (
+
+
+
+ )
+ }
+
+ /**
+ * Render the video URL input.
+ *
+ * @return {Element}
+ */
+
+ renderInput = () => {
+ const video = this.props.node.data.get('video')
+ return (
+
+ )
+ }
+
+}
+
+/**
+ * Export.
+ */
+
+export default Video
diff --git a/examples/index.css b/examples/index.css
index 5fb414c59..ce344a0a6 100644
--- a/examples/index.css
+++ b/examples/index.css
@@ -1,5 +1,7 @@
-html {
+html,
+input,
+textarea {
font-family: 'Roboto', sans-serif;
line-height: 1.4;
background: #eee;
@@ -41,6 +43,19 @@ td {
border: 2px solid #ddd;
}
+input {
+ font-size: .85em;
+ width: 100%;
+ padding: .5em;
+ border: 2px solid #ddd;
+ background: #fafafa;
+}
+
+input:focus {
+ outline: 0;
+ border-color: blue;
+}
+
/**
* Icons.
*/
diff --git a/examples/index.js b/examples/index.js
index 62e40be77..c8bfd5f20 100644
--- a/examples/index.js
+++ b/examples/index.js
@@ -9,6 +9,7 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
import AutoMarkdown from './auto-markdown'
import CodeHighlighting from './code-highlighting'
+import Embeds from './embeds'
import HoveringMenu from './hovering-menu'
import Images from './images'
import Links from './links'
@@ -67,6 +68,7 @@ class App extends React.Component {
{this.renderTab('Hovering Menu', 'hovering-menu')}
{this.renderTab('Links', 'links')}
{this.renderTab('Images', 'images')}
+ {this.renderTab('Embeds', 'embeds')}
{this.renderTab('Tables', 'tables')}
{this.renderTab('Code Highlighting', 'code-highlighting')}
{this.renderTab('Paste HTML', 'paste-html')}
@@ -117,6 +119,7 @@ const router = (
+
diff --git a/examples/test.html b/examples/test.html
index da4f0a0a7..6633a2774 100644
--- a/examples/test.html
+++ b/examples/test.html
@@ -27,6 +27,10 @@
+
+
+
+
- one
- two
diff --git a/lib/components/content.js b/lib/components/content.js
index fa5933979..4204c0b3b 100644
--- a/lib/components/content.js
+++ b/lib/components/content.js
@@ -155,6 +155,7 @@ class Content extends React.Component {
onBeforeInput = (e) => {
if (this.props.readOnly) return
+ if (isNonEditable(e)) return
const data = {}
this.props.onBeforeInput(e, data)
@@ -169,6 +170,7 @@ class Content extends React.Component {
onBlur = (e) => {
if (this.props.readOnly) return
if (this.tmp.isCopying) return
+ if (isNonEditable(e)) return
const data = {}
this.props.onBlur(e, data)
@@ -191,6 +193,8 @@ class Content extends React.Component {
*/
onCompositionStart = (e) => {
+ if (isNonEditable(e)) return
+
this.tmp.isComposing = true
this.tmp.compositions++
}
@@ -204,6 +208,8 @@ class Content extends React.Component {
*/
onCompositionEnd = (e) => {
+ if (isNonEditable(e)) return
+
this.forces++
const count = this.tmp.compositions
@@ -223,6 +229,8 @@ class Content extends React.Component {
*/
onCopy = (e) => {
+ if (isNonEditable(e)) return
+
this.tmp.isCopying = true
window.requestAnimationFrame(() => {
this.tmp.isCopying = false
@@ -243,6 +251,7 @@ class Content extends React.Component {
onCut = (e) => {
if (this.props.readOnly) return
+ if (isNonEditable(e)) return
this.tmp.isCopying = true
window.requestAnimationFrame(() => {
@@ -263,6 +272,8 @@ class Content extends React.Component {
*/
onDragEnd = (e) => {
+ if (isNonEditable(e)) return
+
this.tmp.isDragging = false
this.tmp.isInternalDrag = null
}
@@ -274,6 +285,8 @@ class Content extends React.Component {
*/
onDragOver = (e) => {
+ if (isNonEditable(e)) return
+
const data = e.nativeEvent.dataTransfer
// COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types)
@@ -295,6 +308,8 @@ class Content extends React.Component {
*/
onDragStart = (e) => {
+ if (isNonEditable(e)) return
+
this.tmp.isDragging = true
this.tmp.isInternalDrag = true
const data = e.nativeEvent.dataTransfer
@@ -318,6 +333,8 @@ class Content extends React.Component {
onDrop = (e) => {
if (this.props.readOnly) return
+ if (isNonEditable(e)) return
+
e.preventDefault()
const { state, renderDecorations } = this.props
@@ -403,6 +420,8 @@ class Content extends React.Component {
*/
onInput = (e) => {
+ if (isNonEditable(e)) return
+
let { state, renderDecorations } = this.props
const { selection } = state
const native = window.getSelection()
@@ -454,6 +473,8 @@ class Content extends React.Component {
onKeyDown = (e) => {
if (this.props.readOnly) return
+ if (isNonEditable(e)) return
+
const key = keycode(e.which)
const data = {}
@@ -505,6 +526,8 @@ class Content extends React.Component {
onPaste = (e) => {
if (this.props.readOnly) return
+ if (isNonEditable(e)) return
+
e.preventDefault()
const { clipboardData } = e
@@ -557,6 +580,7 @@ class Content extends React.Component {
if (this.tmp.isRendering) return
if (this.tmp.isCopying) return
if (this.tmp.isComposing) return
+ if (isNonEditable(e)) return
const { state, renderDecorations } = this.props
let { document, selection } = state
@@ -695,6 +719,21 @@ class Content extends React.Component {
}
+/**
+ * Check if an `event` is being fired from inside a non-contentediable child
+ * element, in which case we'll want to ignore it.
+ *
+ * @param {Event} event
+ * @return {Boolean}
+ */
+
+function isNonEditable(event) {
+ const { target, currentTarget } = event
+ const nonEditable = target.closest('[contenteditable="false"]:not([data-void="true"])')
+ const isContained = currentTarget.contains(nonEditable)
+ return isContained
+}
+
/**
* Export.
*/
diff --git a/lib/components/leaf.js b/lib/components/leaf.js
index 83f0790a2..99a18e8c5 100644
--- a/lib/components/leaf.js
+++ b/lib/components/leaf.js
@@ -55,6 +55,7 @@ class Leaf extends React.Component {
return true
}
+ if (state.isBlurred) return false
const { start, end } = OffsetKey.findBounds(index, props.ranges)
return selection.hasEdgeBetween(node, start, end)
}
diff --git a/lib/components/node.js b/lib/components/node.js
index 6458c3d41..5c82b2504 100644
--- a/lib/components/node.js
+++ b/lib/components/node.js
@@ -28,7 +28,7 @@ class Node extends React.Component {
shouldComponentUpdate = (props) => {
return (
props.node != this.props.node ||
- props.state.selection.hasEdgeIn(props.node)
+ (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
)
}
diff --git a/lib/components/text.js b/lib/components/text.js
index 35dea6cac..4175d145b 100644
--- a/lib/components/text.js
+++ b/lib/components/text.js
@@ -32,7 +32,7 @@ class Text extends React.Component {
shouldComponentUpdate(props, state) {
return (
props.node != this.props.node ||
- props.state.selection.hasEdgeIn(props.node)
+ (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
)
}
diff --git a/lib/components/void.js b/lib/components/void.js
index bf684d39f..5c629a180 100644
--- a/lib/components/void.js
+++ b/lib/components/void.js
@@ -47,7 +47,7 @@ class Void extends React.Component {
shouldComponentUpdate = (props, state) => {
return (
props.node != this.props.node ||
- props.state.selection.hasEdgeIn(props.node)
+ (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
)
}
@@ -86,7 +86,11 @@ class Void extends React.Component {
}
return (
-
+
{this.renderSpacer()}
- {children}
+ {children}
)
diff --git a/lib/plugins/core.js b/lib/plugins/core.js
index 5dc5125cb..024e9027c 100644
--- a/lib/plugins/core.js
+++ b/lib/plugins/core.js
@@ -365,6 +365,7 @@ function Plugin(options = {}) {
*/
function onKeyDownBackspace(e, data, state) {
+
// If expanded, delete regularly.
if (state.isExpanded) {
return state
@@ -406,6 +407,7 @@ function Plugin(options = {}) {
*/
function onKeyDownDelete(e, data, state) {
+
// If expanded, delete regularly.
if (state.isExpanded) {
return state