From 5fa616a01dd0d8e26b699a7bea4af7ca3598d0f6 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 2 Aug 2016 11:08:07 +0300 Subject: [PATCH 1/4] Added example skeleton. IFrame component + basic usage --- examples/iframe-rendering/Readme.md | 7 +++ examples/iframe-rendering/index.js | 93 +++++++++++++++++++++++++++++ examples/index.js | 3 + 3 files changed, 103 insertions(+) create mode 100644 examples/iframe-rendering/Readme.md create mode 100644 examples/iframe-rendering/index.js diff --git a/examples/iframe-rendering/Readme.md b/examples/iframe-rendering/Readme.md new file mode 100644 index 000000000..bc30e1aaf --- /dev/null +++ b/examples/iframe-rendering/Readme.md @@ -0,0 +1,7 @@ + +# 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. + +Check out the [Examples readme](..) to see how to run it! diff --git a/examples/iframe-rendering/index.js b/examples/iframe-rendering/index.js new file mode 100644 index 000000000..d55676233 --- /dev/null +++ b/examples/iframe-rendering/index.js @@ -0,0 +1,93 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +function resolveDocument (reactIFrameElementNode) { + const iFrame = ReactDOM.findDOMNode(reactIFrameElementNode); + return iFrame.contentDocument +} + +//appending context to body > div, to suppress react warning +function getRootDiv (doc) { + let rootDiv = doc.querySelector('div#root') + if (!rootDiv) { + rootDiv = doc.createElement('div') + rootDiv.setAttribute('id', 'root') + rootDiv.id = 'root' + rootDiv.setAttribute('style', 'width: 100%; height: 100%') + + doc.body.appendChild(rootDiv) + } + return rootDiv +} + +class IFrame extends React.Component { + + static propTypes = { + head: React.PropTypes.node, + children: React.PropTypes.node, + } + + //rendering plain frame. + render () { + return + } + + componentDidMount = () => { + this.renderContents() + } + + componentDidUpdate = () => { + this.renderContents() + } + + componentWillUnmount = () => this.getDocument().then((doc) => { + ReactDOM.unmountComponentAtNode(doc.body) + if (this.props.head) { + ReactDOM.unmountComponentAtNode(doc.head) + } + }) + + renderContents = () => this.getDocument().then((doc) => { + if (this.props.head) { + ReactDOM.unstable_renderSubtreeIntoContainer(this, this.props.head, doc.head) + } + const rootDiv = getRootDiv(doc) + ReactDOM.unstable_renderSubtreeIntoContainer(this, this.props.children, rootDiv) + }) + + + getDocument = () => new Promise((resolve) => { + const resolveTick = () => { //using arrow function to preserve `this` context + let doc = resolveDocument(this) + if (doc && doc.readyState === 'complete') { + resolve(doc) + } else { + window.requestAnimationFrame(resolveTick) + } + } + resolveTick() + }) + +} + +class IFrameRendering extends React.Component { + + render () { + const bootstrapCDN = + + + + return ( + + ) + } + +} + +export default IFrameRendering diff --git a/examples/index.js b/examples/index.js index d894e8666..f1565354e 100644 --- a/examples/index.js +++ b/examples/index.js @@ -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')} ) } @@ -134,6 +136,7 @@ const router = ( + ) From 186f1a7c6d768ac9189f3edcf5a4a764ca43ddef Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 3 Aug 2016 03:27:33 +0300 Subject: [PATCH 2/4] Core component will deduce the context from the dispatched event.target element --- lib/components/content.js | 17 ++++++++++++----- lib/components/leaf.js | 14 ++++++++------ lib/plugins/core.js | 17 ++++++++++------- lib/utils/find-element-window.js | 12 ++++++++++++ 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 lib/utils/find-element-window.js diff --git a/lib/components/content.js b/lib/components/content.js index 8fd5dc01e..c36c49496 100644 --- a/lib/components/content.js +++ b/lib/components/content.js @@ -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. diff --git a/lib/components/leaf.js b/lib/components/leaf.js index 2b8be2a5e..4a1d27165 100644 --- a/lib/components/leaf.js +++ b/lib/components/leaf.js @@ -3,7 +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. * @@ -135,14 +135,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) @@ -155,7 +157,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) { @@ -170,14 +172,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) diff --git a/lib/plugins/core.js b/lib/plugins/core.js index 4a545e72b..f4083b3b9 100644 --- a/lib/plugins/core.js +++ b/lib/plugins/core.js @@ -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. @@ -28,7 +29,7 @@ function Plugin(options = {}) { const { placeholder, placeholderClassName, - placeholderStyle + placeholderStyle, } = options /** @@ -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) diff --git a/lib/utils/find-element-window.js b/lib/utils/find-element-window.js new file mode 100644 index 000000000..8d32229ae --- /dev/null +++ b/lib/utils/find-element-window.js @@ -0,0 +1,12 @@ +/** + * Find the DOM context for the `node`, fallback to top level global window object. + * + * @param {Node} node + * @return {Element} el + */ +function findElementWindow (domNode) { + const doc = domNode.ownerDocument || domNode + return doc.defaultView || doc.parentWindow +} + +export default findElementWindow From 95eb2c0bdff4397920562474fcfe81c1bc78e8f8 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 3 Aug 2016 03:31:26 +0300 Subject: [PATCH 3/4] Example how to render slate to IFrame --- examples/iframe-rendering/Readme.md | 9 +- examples/iframe-rendering/index.js | 194 ++++++++++++------- examples/iframe-rendering/state.json | 277 +++++++++++++++++++++++++++ package.json | 2 + 4 files changed, 413 insertions(+), 69 deletions(-) create mode 100644 examples/iframe-rendering/state.json diff --git a/examples/iframe-rendering/Readme.md b/examples/iframe-rendering/Readme.md index bc30e1aaf..a4756b524 100644 --- a/examples/iframe-rendering/Readme.md +++ b/examples/iframe-rendering/Readme.md @@ -1,7 +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! diff --git a/examples/iframe-rendering/index.js b/examples/iframe-rendering/index.js index d55676233..708df9956 100644 --- a/examples/iframe-rendering/index.js +++ b/examples/iframe-rendering/index.js @@ -1,77 +1,129 @@ import React from 'react' import ReactDOM from 'react-dom' -function resolveDocument (reactIFrameElementNode) { - const iFrame = ReactDOM.findDOMNode(reactIFrameElementNode); - return iFrame.contentDocument +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' + } } -//appending context to body > div, to suppress react warning -function getRootDiv (doc) { - let rootDiv = doc.querySelector('div#root') - if (!rootDiv) { - rootDiv = doc.createElement('div') - rootDiv.setAttribute('id', 'root') - rootDiv.id = 'root' - rootDiv.setAttribute('style', 'width: 100%; height: 100%') - - doc.body.appendChild(rootDiv) - } - return rootDiv -} - -class IFrame extends React.Component { - - static propTypes = { - head: React.PropTypes.node, - children: React.PropTypes.node, - } - - //rendering plain frame. - render () { - return - } - - componentDidMount = () => { - this.renderContents() - } - - componentDidUpdate = () => { - this.renderContents() - } - - componentWillUnmount = () => this.getDocument().then((doc) => { - ReactDOM.unmountComponentAtNode(doc.body) - if (this.props.head) { - ReactDOM.unmountComponentAtNode(doc.head) - } - }) - - renderContents = () => this.getDocument().then((doc) => { - if (this.props.head) { - ReactDOM.unstable_renderSubtreeIntoContainer(this, this.props.head, doc.head) - } - const rootDiv = getRootDiv(doc) - ReactDOM.unstable_renderSubtreeIntoContainer(this, this.props.children, rootDiv) - }) - - - getDocument = () => new Promise((resolve) => { - const resolveTick = () => { //using arrow function to preserve `this` context - let doc = resolveDocument(this) - if (doc && doc.readyState === 'complete') { - resolve(doc) - } else { - window.requestAnimationFrame(resolveTick) - } - } - resolveTick() - }) - +const NODES = { + 'table': props => {props.children}
, + 'table-row': props => {props.children}, + 'table-cell': props => {props.children} } 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 = return ( - + + + ) } diff --git a/examples/iframe-rendering/state.json b/examples/iframe-rendering/state.json new file mode 100644 index 000000000..d8809df13 --- /dev/null +++ b/examples/iframe-rendering/state.json @@ -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" + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/package.json b/package.json index c2763bef9..3ebd09af4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,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", From 9cb361dc29d928884c1dd578af3d5a3b47da33ef Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 3 Aug 2016 04:03:43 +0300 Subject: [PATCH 4/4] Lint errors fixes. --- examples/iframe-rendering/index.js | 12 +++++++----- examples/index.js | 2 +- lib/components/leaf.js | 3 ++- lib/utils/find-element-window.js | 10 ++++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/examples/iframe-rendering/index.js b/examples/iframe-rendering/index.js index 708df9956..f2f164028 100644 --- a/examples/iframe-rendering/index.js +++ b/examples/iframe-rendering/index.js @@ -1,8 +1,8 @@ import React from 'react' import ReactDOM from 'react-dom' - import injector from 'react-frame-aware-selection-plugin' -injector(); + +injector() import { Editor, Mark, Raw } from '../..' import initialState from './state.json' @@ -124,14 +124,16 @@ class IFrameRendering extends React.Component { return MARKS[mark.type] } - render () { - const bootstrapCDN = + render() { + const bootstrapCDN = ( + crossOrigin="anonymous" + > + ) return ( diff --git a/examples/index.js b/examples/index.js index f1565354e..33b966235 100644 --- a/examples/index.js +++ b/examples/index.js @@ -136,7 +136,7 @@ const router = ( - + ) diff --git a/lib/components/leaf.js b/lib/components/leaf.js index 4a1d27165..855ee695e 100644 --- a/lib/components/leaf.js +++ b/lib/components/leaf.js @@ -4,6 +4,7 @@ import OffsetKey from '../utils/offset-key' import React from 'react' import ReactDOM from 'react-dom' import findElementWindow from '../utils/find-element-window' + /** * Debugger. * @@ -135,7 +136,7 @@ class Leaf extends React.Component { focusOffset = 0 } - const contextWindow = findElementWindow(ReactDOM.findDOMNode(this)); + const contextWindow = findElementWindow(ReactDOM.findDOMNode(this)) // We have a selection to render, so prepare a few things... const native = contextWindow.getSelection() diff --git a/lib/utils/find-element-window.js b/lib/utils/find-element-window.js index 8d32229ae..4bd88147e 100644 --- a/lib/utils/find-element-window.js +++ b/lib/utils/find-element-window.js @@ -1,10 +1,12 @@ + /** - * Find the DOM context for the `node`, fallback to top level global window object. + * Find the window object for the domNode. * - * @param {Node} node - * @return {Element} el + * @param {Node} domNode + * @return {Window} window */ -function findElementWindow (domNode) { + +function findElementWindow(domNode) { const doc = domNode.ownerDocument || domNode return doc.defaultView || doc.parentWindow }