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 + +![](../../docs/images/links-example.png) + +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