1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-04-21 13:51:59 +02:00

add placeholder

This commit is contained in:
Ian Storm Taylor 2016-07-11 18:36:45 -07:00
parent 806ffc38df
commit 3d191dbf14
10 changed files with 399 additions and 201 deletions

@ -34,7 +34,6 @@ dist: $(shell find ./lib) package.json
--out-dir \
./dist \
./lib
@ touch ./dist
# Build the examples.
examples:
@ -84,6 +83,14 @@ test-server:
--fgrep "$(GREP)" \
./test/server.js
# Watch the source.
watch-dist: $(shell find ./lib) package.json
@ $(babel) \
--watch \
--out-dir \
./dist \
./lib
# Watch the examples.
watch-examples:
@ $(watchify) \

@ -66,6 +66,7 @@ class PlainText extends React.Component {
render = () => {
return (
<Editor
placeholder={'Enter some plain text...'}
state={this.state.state}
onChange={this.onChange}
/>

@ -1,5 +1,5 @@
import { Editor, Mark, Raw, Utils } from '../..'
import { Editor, Mark, Placeholder, Raw, Utils } from '../..'
import React from 'react'
import initialState from './state.json'
import keycode from 'keycode'
@ -11,13 +11,12 @@ import keycode from 'keycode'
*/
const NODES = {
'block-quote': props => <blockquote>{props.children}</blockquote>,
'bulleted-list': props => <ul>{props.chidlren}</ul>,
'heading-one': props => <h1>{props.children}</h1>,
'heading-two': props => <h2>{props.children}</h2>,
'list-item': props => <li>{props.chidlren}</li>,
'numbered-list': props => <ol>{props.children}</ol>,
'paragraph': props => <p>{props.children}</p>
'block-quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
'bulleted-list': props => <ul {...props.attributes}>{props.chidlren}</ul>,
'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
'list-item': props => <li {...props.attributes}>{props.chidlren}</li>,
'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>
}
/**
@ -107,6 +106,7 @@ class RichText extends React.Component {
return (
<div className="editor">
<Editor
placeholder={'Enter some rich text...'}
state={this.state.state}
renderNode={this.renderNode}
renderMark={this.renderMark}

@ -387,8 +387,13 @@ class Content extends React.Component {
.map(child => this.renderNode(child))
.toArray()
const attributes = {
'data-key': node.key
}
const element = (
<Component
attributes={attributes}
key={node.key}
editor={editor}
node={node}

@ -1,8 +1,8 @@
import Content from './content'
import CorePlugin from '../plugins/core'
import React from 'react'
import State from '../models/state'
import corePlugin from '../plugins/core'
/**
* Editor.
@ -190,7 +190,6 @@ class Editor extends React.Component {
if (!plugin.renderNode) continue
const component = plugin.renderNode(node, this.state.state, this)
if (component) return component
throw new Error(`No renderer found for node with type "${node.type}".`)
}
}
@ -206,7 +205,6 @@ class Editor extends React.Component {
if (!plugin.renderMark) continue
const style = plugin.renderMark(mark, this.state.state, this)
if (style) return style
throw new Error(`No renderer found for mark with type "${mark.type}".`)
}
}
@ -226,6 +224,7 @@ class Editor extends React.Component {
resolvePlugins = (props) => {
const { onChange, plugins, ...editorPlugin } = props
const corePlugin = CorePlugin(props)
return [
editorPlugin,
...plugins,

@ -0,0 +1,108 @@
import Portal from 'react-portal'
import React from 'react'
import findDOMNode from '../utils/find-dom-node'
/**
* Placeholder.
*/
class Placeholder extends React.Component {
/**
* Properties.
*/
static propTypes = {
children: React.PropTypes.any.isRequired,
className: React.PropTypes.string,
node: React.PropTypes.object.isRequired,
parent: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired,
style: React.PropTypes.object
};
static defaultProps = {
onlyFirstChild: false,
style: {
opacity: '0.333'
}
};
/**
* Should the component update?
*
* @param {Object} props
* @param {Object} state
* @return {Boolean}
*/
shouldComponentUpdate = (props, state) => {
return (
props.children != this.props.children ||
props.className != this.props.className ||
props.parent != this.props.parent ||
props.node != this.props.node ||
props.style != this.props.style
)
}
/**
* Is the placeholder visible?
*
* @return {Boolean}
*/
isVisible = () => {
const { onlyFirstChild, node, parent } = this.props
if (node.text) return false
if (parent.nodes.size > 1) return false
const isFirst = parent.nodes.first() === node
if (isFirst) return true
return false
}
/**
* On open, update the placeholder element's position.
*
* @param {Element} portal
*/
onOpen = (portal) => {
const { node } = this.props
const el = portal.firstChild
const nodeEl = findDOMNode(node)
const rect = nodeEl.getBoundingClientRect()
el.style.pointerEvents = 'none'
el.style.position = 'absolute'
el.style.top = `${rect.top}px`
el.style.left = `${rect.left}px`
el.style.width = `${rect.width}px`
el.style.height = `${rect.height}px`
}
/**
* Render.
*
* @return {Element} element
*/
render = () => {
const { children, className, style } = this.props
const isOpen = this.isVisible()
return (
<Portal isOpened={isOpen} onOpen={this.onOpen}>
<span className={className} style={style}>{children}</span>
</Portal>
)
}
}
/**
* Export.
*/
export default Placeholder

@ -4,6 +4,7 @@
*/
import Editor from './components/editor'
import Placeholder from './components/placeholder'
/**
* Models.
@ -31,8 +32,12 @@ import Raw from './serializers/raw'
*/
import Key from './utils/key'
import findDOMNode from './utils/find-dom-node'
const Utils = { Key }
const Utils = {
Key,
findDOMNode
}
/**
* Export.
@ -47,6 +52,7 @@ export {
Html,
Inline,
Mark,
Placeholder,
Raw,
Selection,
State,
@ -63,6 +69,7 @@ export default {
Html,
Inline,
Mark,
Placeholder,
Raw,
Selection,
State,

@ -485,7 +485,7 @@ const Node = {
if (range.isCollapsed && startOffset == 0) {
const text = this.getDescendant(startKey)
const previous = this.getPreviousText(startKey)
if (!previous) return marks
if (!previous || !previous.length) return marks
const char = previous.characters.get(previous.length - 1)
return char.marks
}

@ -1,204 +1,258 @@
import Key from '../utils/key'
import Placeholder from '../components/placeholder'
import React from 'react'
import keycode from 'keycode'
import { IS_WINDOWS, IS_MAC } from '../utils/environment'
/**
* Default block renderer.
* The default plugin.
*
* @param {Object} props
* @return {Element} element
* @param {Object} options
* @return {Object}
*/
function DEFAULT_BLOCK(props) {
return <div>{props.children}</div>
function Plugin(options = {}) {
const { placeholder } = options
/**
* Define a default block renderer.
*
* @type {Component}
*/
class DEFAULT_BLOCK extends React.Component {
static propTypes = {
attributes: React.PropTypes.object.isRequired,
children: React.PropTypes.any.isRequired,
node: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired
};
render = () => {
const { attributes, children } = this.props
return (
<div {...attributes}>
{this.renderPlaceholder()}
{children}
</div>
)
}
renderPlaceholder = () => {
if (!placeholder) return null
const { node, state } = this.props
return (
<Placeholder parent={state.document} node={node} state={state}>
{placeholder}
</Placeholder>
)
}
}
/**
* Define a default inline renderer.
*
* @type {Component}
*/
class DEFAULT_INLINE extends React.Component {
static propTypes = {
attributes: React.PropTypes.object.isRequired,
children: React.PropTypes.any.isRequired,
node: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired
};
render = () => {
const { attributes, children } = this.props
return <span {...attributes}>{children}</span>
}
}
/**
* Define a default mark renderer.
*
* @type {Object}
*/
const DEFAULT_MARK = {}
/**
* Return the plugin.
*/
return {
/**
* The core `onBeforeInput` handler.
*
* If the current selection is expanded, we have to re-render.
*
* If the next state resolves a new list of decorations for any of its text
* nodes, we have to re-render.
*
* Otherwise, we can allow the default, native text insertion, avoiding a
* re-render for improved performance.
*
* @param {Event} e
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onBeforeInput(e, state, editor) {
const transform = state.transform().insertText(e.data)
const synthetic = transform.apply()
const resolved = editor.resolveState(synthetic)
const isSynthenic = (
state.isExpanded ||
!resolved.equals(synthetic)
)
if (isSynthenic) e.preventDefault()
return isSynthenic
? synthetic
: transform.apply({ isNative: true })
},
/**
* The core `onKeyDown` handler.
*
* @param {Event} e
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onKeyDown(e, state, editor) {
const key = keycode(e.which)
const transform = state.transform()
switch (key) {
case 'enter': {
return transform.splitBlock().apply()
}
case 'backspace': {
return Key.isWord(e)
? transform.backspaceWord().apply()
: transform.deleteBackward().apply()
}
case 'delete': {
return Key.isWord(e)
? transform.deleteWord().apply()
: transform.deleteForward().apply()
}
case 'up': {
if (state.isExpanded) return
const first = state.blocks.first()
if (!first || !first.isVoid) return
e.preventDefault()
return transform.moveToEndOfPreviousBlock().apply()
}
case 'down': {
if (state.isExpanded) return
const first = state.blocks.first()
if (!first || !first.isVoid) return
e.preventDefault()
return transform.moveToStartOfNextBlock().apply()
}
case 'left': {
if (state.isExpanded) return
const node = state.blocks.first() || state.inlines.first()
if (!node || !node.isVoid) return
e.preventDefault()
return transform.moveToEndOfPreviousText().apply()
}
case 'right': {
if (state.isExpanded) return
const node = state.blocks.first() || state.inlines.first()
if (!node || !node.isVoid) return
e.preventDefault()
return transform.moveToStartOfNextText().apply()
}
case 'y': {
if (!Key.isWindowsCommand(e)) return
return transform.redo()
}
case 'z': {
if (!Key.isCommand(e)) return
return IS_MAC && Key.isShift(e)
? transform.redo()
: transform.undo()
}
}
},
/**
* The core `onPaste` handler, which treats everything as plain text.
*
* @param {Event} e
* @param {Object} paste
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onPaste(e, paste, state, editor) {
if (paste.type == 'files') return
let transform = state.transform()
paste.text
.split('\n')
.forEach((line, i) => {
if (i > 0) transform = transform.splitBlock()
transform = transform.insertText(line)
})
return transform.apply()
},
/**
* The core `node` renderer, which uses plain `<div>` or `<span>` depending on
* what kind of node it is.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) {
return node.kind == 'block'
? DEFAULT_BLOCK
: DEFAULT_INLINE
},
/**
* The core `mark` renderer, with no styles.
*
* @param {Mark} mark
* @return {Object} style
*/
renderMark(mark) {
return DEFAULT_MARK
}
}
}
/**
* Default inline renderer.
*
* @param {Object} props
* @return {Element} element
*/
function DEFAULT_INLINE(props) {
return <span>{props.children}</span>
}
/**
* Default mark renderer.
*
* @type {Object}
*/
const DEFAULT_MARK = {}
/**
* Export.
*/
export default {
/**
* The core `onBeforeInput` handler.
*
* If the current selection is expanded, we have to re-render.
*
* If the next state resolves a new list of decorations for any of its text
* nodes, we have to re-render.
*
* Otherwise, we can allow the default, native text insertion, avoiding a
* re-render for improved performance.
*
* @param {Event} e
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onBeforeInput(e, state, editor) {
const transform = state.transform().insertText(e.data)
const synthetic = transform.apply()
const resolved = editor.resolveState(synthetic)
const isSynthenic = (
state.isExpanded ||
!resolved.equals(synthetic)
)
if (isSynthenic) e.preventDefault()
return isSynthenic
? synthetic
: transform.apply({ isNative: true })
},
/**
* The core `onKeyDown` handler.
*
* @param {Event} e
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onKeyDown(e, state, editor) {
const key = keycode(e.which)
const transform = state.transform()
switch (key) {
case 'enter': {
return transform.splitBlock().apply()
}
case 'backspace': {
return Key.isWord(e)
? transform.backspaceWord().apply()
: transform.deleteBackward().apply()
}
case 'delete': {
return Key.isWord(e)
? transform.deleteWord().apply()
: transform.deleteForward().apply()
}
case 'up': {
if (state.isExpanded) return
const first = state.blocks.first()
if (!first || !first.isVoid) return
e.preventDefault()
return transform.moveToEndOfPreviousBlock().apply()
}
case 'down': {
if (state.isExpanded) return
const first = state.blocks.first()
if (!first || !first.isVoid) return
e.preventDefault()
return transform.moveToStartOfNextBlock().apply()
}
case 'left': {
if (state.isExpanded) return
const node = state.blocks.first() || state.inlines.first()
if (!node || !node.isVoid) return
e.preventDefault()
return transform.moveToEndOfPreviousText().apply()
}
case 'right': {
if (state.isExpanded) return
const node = state.blocks.first() || state.inlines.first()
if (!node || !node.isVoid) return
e.preventDefault()
return transform.moveToStartOfNextText().apply()
}
case 'y': {
if (!Key.isWindowsCommand(e)) return
return transform.redo()
}
case 'z': {
if (!Key.isCommand(e)) return
return IS_MAC && Key.isShift(e)
? transform.redo()
: transform.undo()
}
}
},
/**
* The core `onPaste` handler, which treats everything as plain text.
*
* @param {Event} e
* @param {Object} paste
* @param {State} state
* @param {Editor} editor
* @return {State or Null} newState
*/
onPaste(e, paste, state, editor) {
if (paste.type == 'files') return
let transform = state.transform()
paste.text
.split('\n')
.forEach((line, i) => {
if (i > 0) transform = transform.splitBlock()
transform = transform.insertText(line)
})
return transform.apply()
},
/**
* The core `node` renderer, which uses plain `<div>` or `<span>` depending on
* what kind of node it is.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) {
return node.kind == 'block'
? DEFAULT_BLOCK
: DEFAULT_INLINE
},
/**
* The core `mark` renderer, with no styles.
*
* @param {Mark} mark
* @return {Object} style
*/
renderMark(mark) {
return DEFAULT_MARK
}
}
export default Plugin

@ -0,0 +1,17 @@
/**
* Find the DOM node for a `node`.
*
* @param {Node} node
* @return {Element} el
*/
function findDOMNode(node) {
return window.document.querySelector(`[data-key="${node.key}"]`)
}
/**
* Export.
*/
export default findDOMNode