1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-13 18:53:59 +02:00

add embeds example, fix to not let events propagate out of voids

This commit is contained in:
Ian Storm Taylor
2016-07-28 15:38:17 -07:00
parent 39ee56c5c5
commit c248b3de22
13 changed files with 318 additions and 6 deletions

View File

@@ -0,0 +1,9 @@
# Images Example
![](../../docs/images/images-example.png)
This example shows you how you can use "void" nodes to render content that has no text in it, like images.
Check out the [Examples readme](..) to see how to run it!

81
examples/embeds/index.js Normal file
View File

@@ -0,0 +1,81 @@
import { Editor, Raw } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
import Video from './video'
import initialState from './state.json'
/**
* Define a set of node renderers.
*
* @type {Object}
*/
const NODES = {
video: Video
}
/**
* The images example.
*
* @type {Component}
*/
class Embeds 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 })
}
/**
* Render the app.
*
* @return {Element} element
*/
render = () => {
return (
<div className="editor">
<Editor
state={this.state.state}
renderNode={this.renderNode}
onChange={this.onChange}
/>
</div>
)
}
/**
* Render a `node`.
*
* @param {Node} node
* @return {Element}
*/
renderNode = (node) => {
return NODES[node.type]
}
}
/**
* Export.
*/
export default Embeds

View File

@@ -0,0 +1,32 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"text": "In addition to simple image nodes, you can actually create complex embedded nodes. For example, this one contains an input element that lets you change the video being rendered!"
}
]
},
{
"kind": "block",
"type": "video",
"isVoid": true,
"data": {
"video": "https://www.youtube.com/embed/FaHEusBG20c"
}
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"text": "Try it out! If you want another good video URL to try, go with: https://www.youtube.com/embed/6Ejga4kJUts"
}
]
}
]
}

122
examples/embeds/video.js Normal file
View File

@@ -0,0 +1,122 @@
import React from 'react'
import { Void } from '../..'
/**
* An video embed component.
*
* @type {Component}
*/
class Video extends React.Component {
/**
* When the input text changes, update the `video` data on the node.
*
* @param {Event} e
*/
onChange = (e) => {
const video = e.target.value
const { node, state, editor } = this.props
const properties = {
data: { video }
}
const next = state
.transform()
.setNodeByKey(node.key, properties)
.apply()
editor.onChange(next)
}
/**
* When clicks happen in the input, stop propagation so that the void node
* itself isn't focused, since that would unfocus the input.
*
* @type {Event} e
*/
onClick = (e) => {
e.stopPropagation()
}
/**
* Render.
*
* @return {Element}
*/
render = () => {
return (
<Void {...this.props}>
{this.renderVideo()}
{this.renderInput()}
</Void>
)
}
/**
* Render the Youtube iframe, responsively.
*
* @return {Element}
*/
renderVideo = () => {
const video = this.props.node.data.get('video')
const wrapperStyle = {
position: 'relative',
paddingBottom: '66.66%',
paddingTop: '25px',
height: '0'
}
const iframeStyle = {
position: 'absolute',
top: '0px',
left: '0px',
width: '100%',
height: '100%'
}
return (
<div style={wrapperStyle}>
<iframe
id="ytplayer"
type="text/html"
width="640"
height="390"
src={video}
frameBorder="0"
style={iframeStyle}
></iframe>
</div>
)
}
/**
* Render the video URL input.
*
* @return {Element}
*/
renderInput = () => {
const video = this.props.node.data.get('video')
return (
<input
value={video}
onChange={this.onChange}
onClick={this.onClick}
style={{ marginTop: '5px' }}
/>
)
}
}
/**
* Export.
*/
export default Video

View File

@@ -1,5 +1,7 @@
html { html,
input,
textarea {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
line-height: 1.4; line-height: 1.4;
background: #eee; background: #eee;
@@ -41,6 +43,19 @@ td {
border: 2px solid #ddd; border: 2px solid #ddd;
} }
input {
font-size: .85em;
width: 100%;
padding: .5em;
border: 2px solid #ddd;
background: #fafafa;
}
input:focus {
outline: 0;
border-color: blue;
}
/** /**
* Icons. * Icons.
*/ */

View File

@@ -9,6 +9,7 @@ import { Router, Route, Link, IndexRedirect, hashHistory } from 'react-router'
import AutoMarkdown from './auto-markdown' import AutoMarkdown from './auto-markdown'
import CodeHighlighting from './code-highlighting' import CodeHighlighting from './code-highlighting'
import Embeds from './embeds'
import HoveringMenu from './hovering-menu' import HoveringMenu from './hovering-menu'
import Images from './images' import Images from './images'
import Links from './links' import Links from './links'
@@ -67,6 +68,7 @@ class App extends React.Component {
{this.renderTab('Hovering Menu', 'hovering-menu')} {this.renderTab('Hovering Menu', 'hovering-menu')}
{this.renderTab('Links', 'links')} {this.renderTab('Links', 'links')}
{this.renderTab('Images', 'images')} {this.renderTab('Images', 'images')}
{this.renderTab('Embeds', 'embeds')}
{this.renderTab('Tables', 'tables')} {this.renderTab('Tables', 'tables')}
{this.renderTab('Code Highlighting', 'code-highlighting')} {this.renderTab('Code Highlighting', 'code-highlighting')}
{this.renderTab('Paste HTML', 'paste-html')} {this.renderTab('Paste HTML', 'paste-html')}
@@ -117,6 +119,7 @@ const router = (
<IndexRedirect to="rich-text" /> <IndexRedirect to="rich-text" />
<Route path="auto-markdown" component={AutoMarkdown} /> <Route path="auto-markdown" component={AutoMarkdown} />
<Route path="code-highlighting" component={CodeHighlighting} /> <Route path="code-highlighting" component={CodeHighlighting} />
<Route path="embeds" component={Embeds} />
<Route path="hovering-menu" component={HoveringMenu} /> <Route path="hovering-menu" component={HoveringMenu} />
<Route path="images" component={Images} /> <Route path="images" component={Images} />
<Route path="links" component={Links} /> <Route path="links" component={Links} />

View File

@@ -27,6 +27,10 @@
</blockquote> </blockquote>
<p></p> <p></p>
<p></p> <p></p>
<div contenteditable="false">
<div contenteditable>
<input value="test" />
</div>
<ul> <ul>
<li>one</li> <li>one</li>
<li>two</li> <li>two</li>

View File

@@ -155,6 +155,7 @@ class Content extends React.Component {
onBeforeInput = (e) => { onBeforeInput = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (isNonEditable(e)) return
const data = {} const data = {}
this.props.onBeforeInput(e, data) this.props.onBeforeInput(e, data)
@@ -169,6 +170,7 @@ class Content extends React.Component {
onBlur = (e) => { onBlur = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (this.tmp.isCopying) return if (this.tmp.isCopying) return
if (isNonEditable(e)) return
const data = {} const data = {}
this.props.onBlur(e, data) this.props.onBlur(e, data)
@@ -191,6 +193,8 @@ class Content extends React.Component {
*/ */
onCompositionStart = (e) => { onCompositionStart = (e) => {
if (isNonEditable(e)) return
this.tmp.isComposing = true this.tmp.isComposing = true
this.tmp.compositions++ this.tmp.compositions++
} }
@@ -204,6 +208,8 @@ class Content extends React.Component {
*/ */
onCompositionEnd = (e) => { onCompositionEnd = (e) => {
if (isNonEditable(e)) return
this.forces++ this.forces++
const count = this.tmp.compositions const count = this.tmp.compositions
@@ -223,6 +229,8 @@ class Content extends React.Component {
*/ */
onCopy = (e) => { onCopy = (e) => {
if (isNonEditable(e)) return
this.tmp.isCopying = true this.tmp.isCopying = true
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
this.tmp.isCopying = false this.tmp.isCopying = false
@@ -243,6 +251,7 @@ class Content extends React.Component {
onCut = (e) => { onCut = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (isNonEditable(e)) return
this.tmp.isCopying = true this.tmp.isCopying = true
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@@ -263,6 +272,8 @@ class Content extends React.Component {
*/ */
onDragEnd = (e) => { onDragEnd = (e) => {
if (isNonEditable(e)) return
this.tmp.isDragging = false this.tmp.isDragging = false
this.tmp.isInternalDrag = null this.tmp.isInternalDrag = null
} }
@@ -274,6 +285,8 @@ class Content extends React.Component {
*/ */
onDragOver = (e) => { onDragOver = (e) => {
if (isNonEditable(e)) return
const data = e.nativeEvent.dataTransfer const data = e.nativeEvent.dataTransfer
// COMPAT: In Firefox, `types` is array-like. (2016/06/21) // COMPAT: In Firefox, `types` is array-like. (2016/06/21)
const types = Array.from(data.types) const types = Array.from(data.types)
@@ -295,6 +308,8 @@ class Content extends React.Component {
*/ */
onDragStart = (e) => { onDragStart = (e) => {
if (isNonEditable(e)) return
this.tmp.isDragging = true this.tmp.isDragging = true
this.tmp.isInternalDrag = true this.tmp.isInternalDrag = true
const data = e.nativeEvent.dataTransfer const data = e.nativeEvent.dataTransfer
@@ -318,6 +333,8 @@ class Content extends React.Component {
onDrop = (e) => { onDrop = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (isNonEditable(e)) return
e.preventDefault() e.preventDefault()
const { state, renderDecorations } = this.props const { state, renderDecorations } = this.props
@@ -403,6 +420,8 @@ class Content extends React.Component {
*/ */
onInput = (e) => { onInput = (e) => {
if (isNonEditable(e)) return
let { state, renderDecorations } = this.props let { state, renderDecorations } = this.props
const { selection } = state const { selection } = state
const native = window.getSelection() const native = window.getSelection()
@@ -454,6 +473,8 @@ class Content extends React.Component {
onKeyDown = (e) => { onKeyDown = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (isNonEditable(e)) return
const key = keycode(e.which) const key = keycode(e.which)
const data = {} const data = {}
@@ -505,6 +526,8 @@ class Content extends React.Component {
onPaste = (e) => { onPaste = (e) => {
if (this.props.readOnly) return if (this.props.readOnly) return
if (isNonEditable(e)) return
e.preventDefault() e.preventDefault()
const { clipboardData } = e const { clipboardData } = e
@@ -557,6 +580,7 @@ class Content extends React.Component {
if (this.tmp.isRendering) return if (this.tmp.isRendering) return
if (this.tmp.isCopying) return if (this.tmp.isCopying) return
if (this.tmp.isComposing) return if (this.tmp.isComposing) return
if (isNonEditable(e)) return
const { state, renderDecorations } = this.props const { state, renderDecorations } = this.props
let { document, selection } = state let { document, selection } = state
@@ -695,6 +719,21 @@ class Content extends React.Component {
} }
/**
* Check if an `event` is being fired from inside a non-contentediable child
* element, in which case we'll want to ignore it.
*
* @param {Event} event
* @return {Boolean}
*/
function isNonEditable(event) {
const { target, currentTarget } = event
const nonEditable = target.closest('[contenteditable="false"]:not([data-void="true"])')
const isContained = currentTarget.contains(nonEditable)
return isContained
}
/** /**
* Export. * Export.
*/ */

View File

@@ -55,6 +55,7 @@ class Leaf extends React.Component {
return true return true
} }
if (state.isBlurred) return false
const { start, end } = OffsetKey.findBounds(index, props.ranges) const { start, end } = OffsetKey.findBounds(index, props.ranges)
return selection.hasEdgeBetween(node, start, end) return selection.hasEdgeBetween(node, start, end)
} }

View File

@@ -28,7 +28,7 @@ class Node extends React.Component {
shouldComponentUpdate = (props) => { shouldComponentUpdate = (props) => {
return ( return (
props.node != this.props.node || props.node != this.props.node ||
props.state.selection.hasEdgeIn(props.node) (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
) )
} }

View File

@@ -32,7 +32,7 @@ class Text extends React.Component {
shouldComponentUpdate(props, state) { shouldComponentUpdate(props, state) {
return ( return (
props.node != this.props.node || props.node != this.props.node ||
props.state.selection.hasEdgeIn(props.node) (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
) )
} }

View File

@@ -47,7 +47,7 @@ class Void extends React.Component {
shouldComponentUpdate = (props, state) => { shouldComponentUpdate = (props, state) => {
return ( return (
props.node != this.props.node || props.node != this.props.node ||
props.state.selection.hasEdgeIn(props.node) (props.state.isFocused && props.state.selection.hasEdgeIn(props.node))
) )
} }
@@ -86,7 +86,11 @@ class Void extends React.Component {
} }
return ( return (
<Tag contentEditable={false} onClick={this.onClick}> <Tag
contentEditable={false}
data-void="true"
onClick={this.onClick}
>
<Tag <Tag
contentEditable contentEditable
suppressContentEditableWarning suppressContentEditableWarning
@@ -94,7 +98,7 @@ class Void extends React.Component {
style={styles} style={styles}
> >
{this.renderSpacer()} {this.renderSpacer()}
<Tag contentEditable={false} onClick={this.onClick}>{children}</Tag> <Tag contentEditable={false}>{children}</Tag>
</Tag> </Tag>
</Tag> </Tag>
) )

View File

@@ -365,6 +365,7 @@ function Plugin(options = {}) {
*/ */
function onKeyDownBackspace(e, data, state) { function onKeyDownBackspace(e, data, state) {
// If expanded, delete regularly. // If expanded, delete regularly.
if (state.isExpanded) { if (state.isExpanded) {
return state return state
@@ -406,6 +407,7 @@ function Plugin(options = {}) {
*/ */
function onKeyDownDelete(e, data, state) { function onKeyDownDelete(e, data, state) {
// If expanded, delete regularly. // If expanded, delete regularly.
if (state.isExpanded) { if (state.isExpanded) {
return state return state