diff --git a/Readme.md b/Readme.md index b689492ff..54d6d2f13 100644 --- a/Readme.md +++ b/Readme.md @@ -17,11 +17,11 @@ _Slate is currently in **beta**, while work is being done on: cross-browser supp ## Principles -1. **First-class plugins.** The most important factor of Slate is that plugins are first-class entities—the core editor logic is even implemented as its own plugin. This means that you're able to fully customize the editing experience. So you can build complex interations like those found in the Medium or Canvas editors. +1. **First-class plugins.** The most important part of Slate is that plugins are first-class entities—the core editor logic is even implemented as its own plugin. That means you can _completely_ customize the editing experience, to build complex editors like Medium's or Canvas's without having to fight against the library's assumptions. 2. **Schema-less core.** Slate's core logic doesn't assume anything about the schema of the data you'll be editing, which means that there are no assumptions baked into the library that'll trip you up when you need to go beyond basic usage. -3. **Nested document model.** The document model used for Slate is a nested, recursive tree, just like the DOM itself. This means that creating complex components like tables or nested block quotes is possible for advanced use cases. But it's also easy to keep it simple by only using a single level of hierachy. +3. **Nested document model.** The document model used for Slate is a nested, recursive tree, just like the DOM itself. This means that creating complex components like tables or nested block quotes are possible for advanced use cases. But it's also easy to keep it simple by only using a single level of hierachy. 4. **Stateless and immutable.** By using React and Immutable.js, the Slate editor is built in a stateless fashion using immutable data structures, which leads to better performance, and also a much easier time writing plugins. diff --git a/examples/images/index.js b/examples/images/index.js index d722d0fd5..52c500327 100644 --- a/examples/images/index.js +++ b/examples/images/index.js @@ -152,9 +152,11 @@ class Images extends React.Component { switch (node.type) { case 'image': { return (props) => { - const { data } = props.node + const { node, state } = props + const { data } = node + const isActive = state.blocks.includes(node) const src = data.get('src') - return + return } } case 'paragraph': { diff --git a/examples/images/state.json b/examples/images/state.json index 27ca7415b..02e963d4d 100644 --- a/examples/images/state.json +++ b/examples/images/state.json @@ -18,6 +18,7 @@ "kind": "block", "type": "image", "data": { + "isVoid": true, "src": "https://img.washingtonpost.com/wp-apps/imrs.php?src=https://img.washingtonpost.com/news/speaking-of-science/wp-content/uploads/sites/36/2015/10/as12-49-7278-1024x1024.jpg&w=1484" }, "nodes": [ diff --git a/examples/index.css b/examples/index.css index 12c9fbee5..843ac785b 100644 --- a/examples/index.css +++ b/examples/index.css @@ -17,6 +17,11 @@ pre { img { max-width: 100%; + max-height: 20em; +} + +img[data-active="true"] { + box-shadow: 0 0 0 2px blue; } blockquote { diff --git a/examples/test.html b/examples/test.html index cfc711595..fef81487b 100644 --- a/examples/test.html +++ b/examples/test.html @@ -16,6 +16,7 @@

Some text.

+

Some text. diff --git a/lib/components/content.js b/lib/components/content.js index ff1662942..d4527fa24 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -3,6 +3,7 @@ import OffsetKey from '../utils/offset-key' import React from 'react' import ReactDOM from 'react-dom' import Text from './text' +import Void from './void' import keycode from 'keycode' import { Raw } from '..' import { isCommand, isWindowsCommand } from '../utils/event' @@ -18,6 +19,7 @@ class Content extends React.Component { */ static propTypes = { + editor: React.PropTypes.object.isRequired, onBeforeInput: React.PropTypes.func, onChange: React.PropTypes.func, onKeyDown: React.PropTypes.func, @@ -48,10 +50,13 @@ class Content extends React.Component { */ shouldComponentUpdate(props, state) { - if (props.state == this.props.state) return false - if (props.state.document == this.props.state.document) return false - if (props.state.isNative) return false + // if (props.state.isNative) return false return true + // return ( + // props.state != this.props.state || + // props.state.selection != this.props.state.selection || + // props.state.document != this.props.state.document + // ) } /** @@ -291,7 +296,7 @@ class Content extends React.Component { /** * Render the editor content. * - * @return {Component} component + * @return {Element} element */ render() { @@ -328,7 +333,7 @@ class Content extends React.Component { * Render a `node`. * * @param {Node} node - * @return {Component} component + * @return {Element} element */ renderNode(node) { @@ -345,39 +350,67 @@ class Content extends React.Component { * Render an element `node`. * * @param {Node} node - * @return {Component} component + * @return {Element} element */ renderElement(node) { - const { renderNode, state } = this.props + const { editor, renderNode, state } = this.props const Component = renderNode(node) const children = node.nodes .map(child => this.renderNode(child)) .toArray() - return ( + const element = ( {children} ) + + return node.data.get('isVoid') + ? this.renderVoid(element, node) + : element + } + + /** + * Render a void wrapper around an `element` for `node`. + * + * @param {Node} node + * @param {Element} element + * @return {Element} element + */ + + renderVoid(element, node) { + const { editor, state } = this.props + return ( + + {element} + + ) } /** * Render a text `node`. * * @param {Node} node - * @return {Component} component + * @return {Element} element */ renderText(node) { - const { renderMark, renderNode, state } = this.props + const { editor, renderMark, renderNode, state } = this.props return ( this.onChange(state)} renderMark={mark => this.renderMark(mark)} diff --git a/lib/components/text.js b/lib/components/text.js index 20a35c0a4..90a83d0e9 100644 --- a/lib/components/text.js +++ b/lib/components/text.js @@ -12,6 +12,7 @@ import { List } from 'immutable' class Text extends React.Component { static propTypes = { + editor: React.PropTypes.object.isRequired, node: React.PropTypes.object.isRequired, renderMark: React.PropTypes.func.isRequired, state: React.PropTypes.object.isRequired @@ -54,13 +55,13 @@ class Text extends React.Component { return ( ) } diff --git a/lib/components/void.js b/lib/components/void.js new file mode 100644 index 000000000..462064dea --- /dev/null +++ b/lib/components/void.js @@ -0,0 +1,121 @@ + +import Leaf from './leaf' +import Mark from '../models/mark' +import OffsetKey from '../utils/offset-key' +import React from 'react' +import ReactDOM from 'react-dom' +import keycode from 'keycode' + +/** + * Void. + */ + +class Void extends React.Component { + + static propTypes = { + children: React.PropTypes.any.isRequired, + editor: React.PropTypes.object.isRequired, + node: React.PropTypes.object.isRequired, + state: React.PropTypes.object.isRequired + }; + + onClick(e) { + e.preventDefault() + let { editor, node, state } = this.props + let text = node.getTextNodes().first() + + state = state + .transform() + .moveToStartOf(text) + .apply() + + editor.onChange(state) + } + + onKeyDown(e) { + let { state, editor } = this.props + const key = keycode(e) + + switch (key) { + default: + return + case 'left arrow': + case 'up arrow': + state = state + .transform() + .moveToEndOfPreviousBlock() + .apply() + case 'right arrow': + case 'down arrow': + state = state + .transform() + .moveToStartOfNextBlock() + .apply() + } + + e.preventDefault() + editor.onChange(state) + } + + render() { + const { children, node } = this.props + const Tag = node.kind == 'block' ? 'div' : 'span' + const style = { + position: 'relative' + } + + return ( + this.onClick(e)}> + {this.renderSpacer()} + {children} + + ) + } + + renderSpacer() { + const style = { + position: 'absolute', + top: '0px', // vertically the same, to not scroll the window + left: '-10000px' + } + + return ( + {this.renderLeaf()} + ) + } + + renderLeaf() { + const { node, state } = this.props + const child = node.getTextNodes().first() + const text = '' + const marks = Mark.createSet() + const start = 0 + const end = 0 + const offsetKey = OffsetKey.stringify({ + key: child.key, + start, + end + }) + + return ( + this.leaf = el} + key={offsetKey} + state={state} + node={child} + start={start} + end={end} + text={text} + marks={marks} + renderMark={mark => {}} + /> + ) + } + +} + +/** + * Export. + */ + +export default Void diff --git a/lib/utils/offset-key.js b/lib/utils/offset-key.js index 9ffdc049c..908a976d6 100644 --- a/lib/utils/offset-key.js +++ b/lib/utils/offset-key.js @@ -30,6 +30,12 @@ function findKey(node) { const child = node.querySelector(SELECTOR) if (child) return child.getAttribute(ATTRIBUTE) + // Otherwise, move up the tree looking for cousin offset keys in parents. + while (node = node.parentNode) { + const cousin = node.querySelector(SELECTOR) + if (cousin) return cousin.getAttribute(ATTRIBUTE) + } + // Shouldn't get here... else we have an edge case to handle. console.error('No offset key found for node:', node) } @@ -45,9 +51,15 @@ function findKey(node) { function findPoint(node, offset) { const key = findKey(node) const parsed = parse(key) + + // Don't let the offset be outside the start and end bounds. + offset = parsed.start + offset + offset = Math.max(offset, parsed.start) + offset = Math.min(offset, parsed.end) + return { key: parsed.key, - offset: parsed.start + offset + offset } }