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

Merge branch 'add/render-to-iframe' of https://github.com/vleletko/slate into add-iframe

This commit is contained in:
Ian Storm Taylor 2016-08-05 10:46:40 -07:00
commit 30453d6821
9 changed files with 492 additions and 16 deletions

View File

@ -0,0 +1,14 @@
# IFrame rendering example
This example shows how to render Slate into IFrame, preserving single react component tree.
You may need this if you want to have separate styles for editor content & application.
In example this exmaple you can see,
that editor is using bootstrap styles, while they are not included to parent page.
## React onSelect problem
Current react version has a problem with onSelect event handling, if input is rendered from parent component tree to iframe.
This problem is solved by custom SelectEventPlugin - [react-frame-aware-selection-plugin](https://www.npmjs.com/package/react-frame-aware-selection-plugin)
Check out the [Examples readme](..) to see how to run it!

View File

@ -0,0 +1,153 @@
import React from 'react'
import ReactDOM from 'react-dom'
import injector from 'react-frame-aware-selection-plugin'
injector()
import { Editor, Mark, Raw } from '../..'
import initialState from './state.json'
import Frame from 'react-frame-component'
const MARKS = {
bold: {
fontWeight: 'bold'
},
italic: {
fontStyle: 'italic'
}
}
const NODES = {
'table': props => <table className={"table"}><tbody {...props.attributes}>{props.children}</tbody></table>,
'table-row': props => <tr {...props.attributes}>{props.children}</tr>,
'table-cell': props => <td {...props.attributes}>{props.children}</td>
}
class IFrameRendering extends React.Component {
state = {
state: Raw.deserialize(initialState, { terse: true })
};
onChange = (state) => {
this.setState({ state })
}
/**
* On backspace, do nothing if at the start of a table cell.
*
* @param {Event} e
* @param {State} state
* @return {State or Null} state
*/
onBackspace = (e, state) => {
if (state.startOffset != 0) return
e.preventDefault()
return state
}
/**
* On change.
*
* @param {State} state
*/
onChange = (state) => {
this.setState({ state })
}
/**
* On delete, do nothing if at the end of a table cell.
*
* @param {Event} e
* @param {State} state
* @return {State or Null} state
*/
onDelete = (e, state) => {
if (state.endOffset != state.startText.length) return
e.preventDefault()
return state
}
/**
* On return, do nothing if inside a table cell.
*
* @param {Event} e
* @param {State} state
* @return {State or Null} state
*/
onEnter = (e, state) => {
e.preventDefault()
return state
}
/**
* On key down, check for our specific key shortcuts.
*
* @param {Event} e
* @param {Object} data
* @param {State} state
* @return {State or Null} state
*/
onKeyDown = (e, data, state) => {
if (state.startBlock.type != 'table-cell') return
switch (data.key) {
case 'backspace': return this.onBackspace(e, state)
case 'delete': return this.onDelete(e, state)
case 'enter': return this.onEnter(e, state)
}
}
/**
* Return a node renderer for a Slate `node`.
*
* @param {Node} node
* @return {Component or Void}
*/
renderNode = (node) => {
return NODES[node.type]
}
/**
* Return a mark renderer for a Slate `mark`.
*
* @param {Mark} mark
* @return {Object or Void}
*/
renderMark = (mark) => {
return MARKS[mark.type]
}
render() {
const bootstrapCDN = (
<link
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossOrigin="anonymous"
>
</link>
)
return (
<Frame head={bootstrapCDN} style={{width: `100%`, height: '300px'}}>
<Editor
state={this.state.state}
onChange={this.onChange}
renderNode={this.renderNode}
renderMark={this.renderMark}
onKeyDown={this.onKeyDown}
/>
</Frame>
)
}
}
export default IFrameRendering

View File

@ -0,0 +1,277 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "This example shows how you can render editor in frame component."
}
]
}
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "Check out this table, it has bootstrap styles, and top level document is not polluted by "
},
{
"text": "bootstrap.min.css",
"marks": [
{
"type": "italic"
},
{
"type": "bold"
}
]
},
{
"text": "."
}
]
}
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": ""
}
]
}
]
},
{
"kind": "block",
"type": "table",
"nodes": [
{
"kind": "block",
"type": "table-row",
"nodes": [
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": ""
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "Human",
"marks": [
{
"type": "bold"
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "Dog",
"marks": [
{
"type": "bold"
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "Cat",
"marks": [
{
"type": "bold"
}
]
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-row",
"nodes": [
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "# of Feet",
"marks": [
{
"type": "bold"
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "1"
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "4"
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "4"
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-row",
"nodes": [
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "# of Lives",
"marks": [
{
"type": "bold"
}
]
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "1"
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "1"
}
]
}
]
},
{
"kind": "block",
"type": "table-cell",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "9"
}
]
}
]
}
]
}
]
}
]
}

View File

@ -22,6 +22,7 @@ import RTL from './rtl'
import Tables from './tables'
import DevPerformancePlain from './development/performance-plain'
import DevPerformanceRich from './development/performance-rich'
import IFrameRendering from './iframe-rendering'
/**
* Perf.
@ -76,6 +77,7 @@ class App extends React.Component {
{this.renderTab('Read-only', 'read-only')}
{this.renderTab('RTL', 'rtl')}
{this.renderTab('Plugins', 'plugins')}
{this.renderTab('IFrame', 'iframe')}
</div>
)
}
@ -134,6 +136,7 @@ const router = (
<Route path="tables" component={Tables} />
<Route path="dev-performance-plain" component={DevPerformancePlain} />
<Route path="dev-performance-rich" component={DevPerformanceRich} />
<Route path="iframe" component={IFrameRendering} />
</Route>
</Router>
)

View File

@ -9,6 +9,7 @@ import TYPES from '../utils/types'
import includes from 'lodash/includes'
import keycode from 'keycode'
import { IS_FIREFOX, IS_MAC } from '../utils/environment'
import findElementWindow from '../utils/find-element-window'
/**
* Debug.
@ -380,11 +381,13 @@ class Content extends React.Component {
// Resolve the point where the drop occured.
let range
const contextWindow = findElementWindow(e.nativeEvent.target)
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (window.document.caretRangeFromPoint) {
range = window.document.caretRangeFromPoint(x, y)
if (contextWindow.document.caretRangeFromPoint) {
range = contextWindow.document.caretRangeFromPoint(x, y)
} else {
range = window.document.createRange()
range = contextWindow.document.createRange()
range.setStart(e.nativeEvent.rangeParent, e.nativeEvent.rangeOffset)
}
@ -459,9 +462,11 @@ class Content extends React.Component {
if (isNonEditable(e)) return
debug('onInput')
const contextWindow = findElementWindow(e.nativeEvent.target)
let { state, renderDecorations } = this.props
const { selection } = state
const native = window.getSelection()
const native = contextWindow.getSelection()
const { anchorNode, anchorOffset, focusOffset } = native
const point = this.getPoint(anchorNode, anchorOffset)
const { key, index, start, end } = point
@ -624,9 +629,11 @@ class Content extends React.Component {
if (this.tmp.isComposing) return
if (isNonEditable(e)) return
const contextWindow = findElementWindow(e.nativeEvent.target)
const { state, renderDecorations } = this.props
let { document, selection } = state
const native = window.getSelection()
const native = contextWindow.getSelection()
const data = {}
// If there are no ranges, the editor was blurred natively.

View File

@ -3,6 +3,7 @@ import Debug from 'debug'
import OffsetKey from '../utils/offset-key'
import React from 'react'
import ReactDOM from 'react-dom'
import findElementWindow from '../utils/find-element-window'
/**
* Debugger.
@ -144,14 +145,16 @@ class Leaf extends React.Component {
focusOffset = 0
}
const contextWindow = findElementWindow(ReactDOM.findDOMNode(this))
// We have a selection to render, so prepare a few things...
const native = window.getSelection()
const native = contextWindow.getSelection()
const el = findDeepestNode(ReactDOM.findDOMNode(this))
// If both the start and end are here, set the selection all at once.
if (hasAnchor && hasFocus) {
native.removeAllRanges()
const range = window.document.createRange()
const range = contextWindow.document.createRange()
range.setStart(el, anchorOffset - start)
native.addRange(range)
native.extend(el, focusOffset - start)
@ -164,7 +167,7 @@ class Leaf extends React.Component {
if (selection.isForward) {
if (hasAnchor) {
native.removeAllRanges()
const range = window.document.createRange()
const range = contextWindow.document.createRange()
range.setStart(el, anchorOffset - start)
native.addRange(range)
} else if (hasFocus) {
@ -179,14 +182,14 @@ class Leaf extends React.Component {
else {
if (hasFocus) {
native.removeAllRanges()
const range = window.document.createRange()
const range = contextWindow.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()
const range = contextWindow.document.createRange()
range.setStart(el, anchorOffset - start)
native.addRange(range)
native.extend(endNode, endOffset)

View File

@ -5,6 +5,7 @@ import Debug from 'debug'
import Placeholder from '../components/placeholder'
import React from 'react'
import String from '../utils/string'
import findElementWindow from '../utils/find-element-window'
/**
* Debug.
@ -204,7 +205,9 @@ function Plugin(options = {}) {
*/
function onCutOrCopy(e, data, state) {
const native = window.getSelection()
const contextWindow = findElementWindow(e.nativeEvent.target)
const native = contextWindow.getSelection()
if (!native.rangeCount) return
const { fragment } = data
@ -214,10 +217,10 @@ function Plugin(options = {}) {
// fragment attached as an attribute, so it will show up in the copied HTML.
const range = native.getRangeAt(0)
const contents = range.cloneContents()
const wrapper = window.document.createElement('span')
const wrapper = contextWindow.document.createElement('span')
const text = contents.childNodes[0]
const char = text.textContent.slice(0, 1)
const first = window.document.createTextNode(char)
const first = contextWindow.document.createTextNode(char)
const rest = text.textContent.slice(1)
text.textContent = rest
wrapper.appendChild(first)
@ -225,8 +228,8 @@ function Plugin(options = {}) {
contents.insertBefore(wrapper, text)
// Add the phony content to the DOM, and select it, so it will be copied.
const body = window.document.querySelector('body')
const div = window.document.createElement('div')
const body = contextWindow.document.querySelector('body')
const div = contextWindow.document.createElement('div')
div.setAttribute('contenteditable', true)
div.style.position = 'absolute'
div.style.left = '-9999px'
@ -235,7 +238,7 @@ function Plugin(options = {}) {
// COMPAT: In Firefox, trying to use the terser `native.selectAllChildren`
// throws an error, so we use the older `range` equivalent. (2016/06/21)
const r = window.document.createRange()
const r = contextWindow.document.createRange()
r.selectNodeContents(div)
native.removeAllRanges()
native.addRange(r)

View File

@ -0,0 +1,14 @@
/**
* Find the window object for the domNode.
*
* @param {Node} domNode
* @return {Window} window
*/
function findElementWindow(domNode) {
const doc = domNode.ownerDocument || domNode
return doc.defaultView || doc.parentWindow
}
export default findElementWindow

View File

@ -49,6 +49,8 @@
"react": "^15.2.0",
"react-addons-perf": "^15.2.1",
"react-dom": "^15.1.0",
"react-frame-aware-selection-plugin": "0.0.1",
"react-frame-component": "^0.6.2",
"react-router": "^2.5.1",
"read-metadata": "^1.0.0",
"selection-position": "^1.0.0",