diff --git a/examples/focus-blur/Readme.md b/examples/focus-blur/Readme.md
new file mode 100644
index 000000000..04b913be4
--- /dev/null
+++ b/examples/focus-blur/Readme.md
@@ -0,0 +1,8 @@
+
+# Links Example
+
+
+
+This example shows you how you can wrap text in "inline" nodes to associate metadata, like an `href`, with a piece of text. This is how you'd add links to Slate, but it's also how you might add hashtags, at-mentions, and many more inline features!
+
+Check out the [Examples readme](..) to see how to run it!
diff --git a/examples/focus-blur/index.js b/examples/focus-blur/index.js
new file mode 100644
index 000000000..311be90d6
--- /dev/null
+++ b/examples/focus-blur/index.js
@@ -0,0 +1,145 @@
+
+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'
+
+/**
+ * Define a schema.
+ *
+ * @type {Object}
+ */
+
+const schema = {
+ nodes: {
+ paragraph: props =>
{props.children}
+ }
+}
+
+/**
+ * The focus and blur example.
+ *
+ * @type {Component}
+ */
+
+class FocusBlur 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 })
+ }
+
+ /**
+ * Apply a focus or blur transform by `name` after a `timeout`.
+ *
+ * @param {String} name
+ * @param {Number} timeout
+ */
+
+ onClick = (e, name, timeout = 0) => {
+ e.preventDefault()
+
+ setTimeout(() => {
+ const state = this.state.state
+ .transform()
+ [name]()
+ .apply()
+
+ this.setState({ state })
+ }, timeout)
+ }
+
+ /**
+ * Generate focus and blur button handlers.
+ *
+ * @param {Event} e
+ */
+
+ onClickFocus = e => this.onClick(e, 'focus')
+ onClickFocusDelay = e => this.onClick(e, 'focus', 3000)
+ onClickBlur = e => this.onClick(e, 'blur')
+ onClickBlurDelay = e => this.onClick(e, 'blur', 3000)
+
+ /**
+ * Render the app.
+ *
+ * @return {Element} element
+ */
+
+ render = () => {
+ return (
+
+ {this.renderToolbar()}
+ {this.renderEditor()}
+
+ )
+ }
+
+ /**
+ * Render the toolbar.
+ *
+ * @return {Element} element
+ */
+
+ renderToolbar = () => {
+ return (
+
+
+ done Focus
+
+
+ timer Focus
+
+
+ done Blur
+
+
+ timer Blur
+
+
+ )
+ }
+
+ /**
+ * Render the editor.
+ *
+ * @return {Element} element
+ */
+
+ renderEditor = () => {
+ return (
+
+
+
+ )
+ }
+
+}
+
+/**
+ * Export.
+ */
+
+export default FocusBlur
diff --git a/examples/focus-blur/state.json b/examples/focus-blur/state.json
new file mode 100644
index 000000000..7338c109b
--- /dev/null
+++ b/examples/focus-blur/state.json
@@ -0,0 +1,24 @@
+{
+ "nodes": [
+ {
+ "kind": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "kind": "text",
+ "text": "This is a testing ground for focusing and blurring in Slate."
+ }
+ ]
+ },
+ {
+ "kind": "block",
+ "type": "paragraph",
+ "nodes": [
+ {
+ "kind": "text",
+ "text": "You can use the toolbar buttons above to focus and blur immediately, or after a short delay."
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/index.css b/examples/index.css
index ce2ab9406..94ddda65f 100644
--- a/examples/index.css
+++ b/examples/index.css
@@ -79,6 +79,7 @@ input:focus {
.material-icons {
font-size: 18px;
+ vertical-align: text-bottom;
}
/**
@@ -126,7 +127,7 @@ input:focus {
}
.menu > * + * {
- margin-left: 10px;
+ margin-left: 15px;
}
.button {
diff --git a/examples/index.js b/examples/index.js
index cb938132b..5b88ab660 100644
--- a/examples/index.js
+++ b/examples/index.js
@@ -11,6 +11,7 @@ import AutoMarkdown from './auto-markdown'
import CodeHighlighting from './code-highlighting'
import Embeds from './embeds'
import Emojis from './emojis'
+import FocusBlur from './focus-blur'
import HoveringMenu from './hovering-menu'
import Iframes from './iframes'
import Images from './images'
@@ -81,6 +82,7 @@ class App extends React.Component {
{this.renderTab('RTL', 'rtl')}
{this.renderTab('Plugins', 'plugins')}
{this.renderTab('Iframes', 'iframes')}
+ {this.renderTab('Focus & Blur', 'focus-blur')}
)
}
@@ -129,6 +131,7 @@ const router = (
+
diff --git a/src/components/content.js b/src/components/content.js
index af5dd0039..6dba860a7 100644
--- a/src/components/content.js
+++ b/src/components/content.js
@@ -4,12 +4,14 @@ import Debug from 'debug'
import Node from './node'
import OffsetKey from '../utils/offset-key'
import React from 'react'
+import ReactDOM from 'react-dom'
import Selection from '../models/selection'
import Transfer from '../utils/transfer'
import TYPES from '../constants/types'
import getWindow from 'get-window'
import includes from 'lodash/includes'
import keycode from 'keycode'
+import noop from '../utils/noop'
import { IS_FIREFOX, IS_MAC } from '../constants/environment'
/**
@@ -20,14 +22,6 @@ import { IS_FIREFOX, IS_MAC } from '../constants/environment'
const debug = Debug('slate:content')
-/**
- * Noop.
- *
- * @type {Function}
- */
-
-function noop() {}
-
/**
* Content.
*
@@ -122,16 +116,29 @@ class Content extends React.Component {
}
/**
- * When finished rendering, move the `isRendering` flag on next tick.
+ * When finished rendering, move the `isRendering` flag on next tick and
+ * clean up the DOM's activeElement if neccessary.
*
- * @param {Object} props
- * @param {Object} state
+ * @param {Object} prevProps
+ * @param {Object} prevState
*/
- componentDidUpdate = (props, state) => {
+ componentDidUpdate = (prevProps, prevState) => {
setTimeout(() => {
this.tmp.isRendering = false
}, 1)
+
+ // If the state is blurred now, but was focused before, and the DOM still
+ // has a node inside the editor selected, we need to blur it.
+ if (this.props.state.isBlurred && prevProps.state.isFocused) {
+ const el = ReactDOM.findDOMNode(this)
+ const window = getWindow(el)
+ const native = window.getSelection()
+ if (!el.contains(native.anchorNode)) return
+
+ native.removeAllRanges()
+ el.blur()
+ }
}
/**
diff --git a/src/components/editor.js b/src/components/editor.js
index 68680b590..0c9b70c9c 100644
--- a/src/components/editor.js
+++ b/src/components/editor.js
@@ -6,6 +6,7 @@ import React from 'react'
import Schema from '../models/schema'
import State from '../models/state'
import isReactComponent from '../utils/is-react-component'
+import noop from '../utils/noop'
import typeOf from 'type-of'
/**
@@ -14,14 +15,6 @@ import typeOf from 'type-of'
const debug = Debug('slate:editor')
-/**
- * Noop.
- *
- * @type {Function}
- */
-
-function noop() {}
-
/**
* Event handlers to mix in to the editor.
*
diff --git a/src/components/leaf.js b/src/components/leaf.js
index d648b4306..d1c24368b 100644
--- a/src/components/leaf.js
+++ b/src/components/leaf.js
@@ -156,9 +156,17 @@ class Leaf extends React.Component {
}
// We have a selection to render, so prepare a few things...
- const el = findDeepestNode(ReactDOM.findDOMNode(this))
+ const ref = ReactDOM.findDOMNode(this)
+ const el = findDeepestNode(ref)
const window = getWindow(el)
const native = window.getSelection()
+ const parent = ref.closest('[contenteditable]')
+
+ // COMPAT: In Firefox, it's not enough to create a range, you also need to
+ // focus the contenteditable element. (2016/11/16)
+ function focus() {
+ if (parent) setTimeout(() => parent.focus())
+ }
// If both the start and end are here, set the selection all at once.
if (hasAnchor && hasFocus) {
@@ -167,41 +175,46 @@ class Leaf extends React.Component {
range.setStart(el, anchorOffset - start)
native.addRange(range)
native.extend(el, focusOffset - start)
- return
+ focus()
}
- // If the selection is forward, we can set things in sequence. In
- // the first leaf to render, reset the selection and set the new start. And
- // then in the second leaf to render, extend to the new end.
- if (selection.isForward) {
- if (hasAnchor) {
- native.removeAllRanges()
- const range = window.document.createRange()
- range.setStart(el, anchorOffset - start)
- native.addRange(range)
- } else if (hasFocus) {
- native.extend(el, focusOffset - start)
- }
- }
-
- // Otherwise, if the selection is backward, we need to hack the order a bit.
- // In the first leaf to render, set a phony start anchor to store the true
- // end position. And then in the second leaf to render, set the start and
- // extend the end to the stored value.
+ // Otherwise we need to set the selection across two different leaves.
else {
- if (hasFocus) {
- native.removeAllRanges()
- const range = window.document.createRange()
- range.setStart(el, focusOffset - start)
- native.addRange(range)
- } else if (hasAnchor) {
- const endNode = native.focusNode
- const endOffset = native.focusOffset
- native.removeAllRanges()
- const range = window.document.createRange()
- range.setStart(el, anchorOffset - start)
- native.addRange(range)
- native.extend(endNode, endOffset)
+ // If the selection is forward, we can set things in sequence. In the
+ // first leaf to render, reset the selection and set the new start. And
+ // then in the second leaf to render, extend to the new end.
+ if (selection.isForward) {
+ if (hasAnchor) {
+ native.removeAllRanges()
+ const range = window.document.createRange()
+ range.setStart(el, anchorOffset - start)
+ native.addRange(range)
+ } else if (hasFocus) {
+ native.extend(el, focusOffset - start)
+ focus()
+ }
+ }
+
+ // Otherwise, if the selection is backward, we need to hack the order a bit.
+ // In the first leaf to render, set a phony start anchor to store the true
+ // end position. And then in the second leaf to render, set the start and
+ // extend the end to the stored value.
+ else {
+ if (hasFocus) {
+ native.removeAllRanges()
+ const range = window.document.createRange()
+ range.setStart(el, focusOffset - start)
+ native.addRange(range)
+ } else if (hasAnchor) {
+ const endNode = native.focusNode
+ const endOffset = native.focusOffset
+ native.removeAllRanges()
+ const range = window.document.createRange()
+ range.setStart(el, anchorOffset - start)
+ native.addRange(range)
+ native.extend(endNode, endOffset)
+ focus()
+ }
}
}
diff --git a/src/components/void.js b/src/components/void.js
index b727c35be..2abc602c4 100644
--- a/src/components/void.js
+++ b/src/components/void.js
@@ -5,16 +5,9 @@ import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import keycode from 'keycode'
+import noop from '../utils/noop'
import { IS_FIREFOX } from '../constants/environment'
-/**
- * Noop.
- *
- * @type {Function}
- */
-
-function noop() {}
-
/**
* Void.
*
diff --git a/src/utils/noop.js b/src/utils/noop.js
new file mode 100644
index 000000000..2ef804c31
--- /dev/null
+++ b/src/utils/noop.js
@@ -0,0 +1,14 @@
+
+/**
+ * Noop.
+ *
+ * @return {Undefined}
+ */
+
+function noop() {}
+
+/**
+ * Export.
+ */
+
+export default noop