mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-01-17 21:49:20 +01:00
got void components working
This commit is contained in:
parent
27e71715a6
commit
aba40a2aaf
@ -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.
|
||||
|
||||
|
@ -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 <img src={src} />
|
||||
return <img src={src} data-active={isActive} />
|
||||
}
|
||||
}
|
||||
case 'paragraph': {
|
||||
|
@ -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": [
|
||||
|
@ -17,6 +17,11 @@ pre {
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
img[data-active="true"] {
|
||||
box-shadow: 0 0 0 2px blue;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
|
@ -16,6 +16,7 @@
|
||||
<p>
|
||||
<strong>Some text.</strong>
|
||||
</p>
|
||||
<img 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" />
|
||||
<blockquote>
|
||||
<p>
|
||||
<strong>Some <a href="google">text.</a></strong>
|
||||
|
@ -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 = (
|
||||
<Component
|
||||
key={node.key}
|
||||
editor={editor}
|
||||
node={node}
|
||||
state={state}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
|
||||
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 (
|
||||
<Void
|
||||
key={node.key}
|
||||
editor={editor}
|
||||
node={node}
|
||||
state={state}
|
||||
>
|
||||
{element}
|
||||
</Void>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Text
|
||||
key={node.key}
|
||||
editor={editor}
|
||||
node={node}
|
||||
renderMark={renderMark}
|
||||
state={state}
|
||||
|
@ -102,6 +102,7 @@ class Editor extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Content
|
||||
editor={this}
|
||||
state={this.props.state}
|
||||
onChange={state => this.onChange(state)}
|
||||
renderMark={mark => this.renderMark(mark)}
|
||||
|
@ -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 (
|
||||
<Leaf
|
||||
key={offsetKey}
|
||||
marks={marks}
|
||||
state={state}
|
||||
node={node}
|
||||
start={start}
|
||||
end={end}
|
||||
renderMark={renderMark}
|
||||
state={state}
|
||||
text={text}
|
||||
marks={marks}
|
||||
renderMark={renderMark}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
121
lib/components/void.js
Normal file
121
lib/components/void.js
Normal file
@ -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 (
|
||||
<Tag style={style} onClick={e => this.onClick(e)}>
|
||||
{this.renderSpacer()}
|
||||
<span contenteditable={false}>{children}</span>
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
||||
renderSpacer() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: '0px', // vertically the same, to not scroll the window
|
||||
left: '-10000px'
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style}>{this.renderLeaf()}</span>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<Leaf
|
||||
ref={el => this.leaf = el}
|
||||
key={offsetKey}
|
||||
state={state}
|
||||
node={child}
|
||||
start={start}
|
||||
end={end}
|
||||
text={text}
|
||||
marks={marks}
|
||||
renderMark={mark => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Export.
|
||||
*/
|
||||
|
||||
export default Void
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user