diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index 1c04fba8..ecf523ed 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,63 +1,44 @@ -import React, { Component, PropTypes } from 'react'; +import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { ScrollSync, ScrollSyncPane } from '../ScrollSync'; import ControlPane from '../ControlPanel/ControlPane'; import PreviewPane from '../PreviewPane/PreviewPane'; import styles from './EntryEditor.css'; -export default class EntryEditor extends Component { - - state = { - scrollTop: 0, - scrollHeight: 0, - offsetHeight: 0, - } - - handleControlPaneScroll = evt => { - const { scrollTop, scrollHeight, offsetHeight } = evt.target; - this.setState({ - scrollTop, - scrollHeight, - offsetHeight, - }); - } - - render() { - const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; - const { scrollTop, scrollHeight, offsetHeight } = this.state; - - return ( -
+export default function EntryEditor( + { + collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist + }) { + return ( +
+
-
- -
+ +
+ +
+
-
- -
+
+
+
- ); - } +
+ ); } EntryEditor.propTypes = { diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index e97348bb..8134d0a8 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; -import { render } from 'react-dom'; +import ReactDOM from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { ScrollSyncPane } from '../ScrollSync'; import registry from '../../lib/registry'; import { resolveWidget } from '../Widgets'; import Preview from './Preview'; @@ -8,16 +9,8 @@ import styles from './PreviewPane.css'; export default class PreviewPane extends React.Component { - componentDidUpdate(prevProps) { - // Update scroll position of the iframe - const { scrollTop, scrollHeight, offsetHeight, ...rest } = this.props; - const frameHeight = this.iframeBody.scrollHeight - offsetHeight; - this.iframeBody.scrollTop = frameHeight * scrollTop / (scrollHeight - offsetHeight); - - // We don't want to re-render on scroll - if (prevProps.collection !== this.props.collection || prevProps.entry !== this.props.entry) { - this.renderPreview(rest); - } + componentDidUpdate() { + this.renderPreview(); } widgetFor = name => { @@ -30,15 +23,21 @@ export default class PreviewPane extends React.Component { field, getMedia, }); - } + }; - renderPreview(props) { - const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview; + renderPreview() { + const component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview; const previewProps = { - ...props, + ...this.props, widgetFor: this.widgetFor }; - render(React.createElement(component, previewProps), this.previewEl); + // We need to use this API in order to pass context to the iframe + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + + {React.createElement(component, previewProps)} + + , this.previewEl); } handleIframeRef = ref => { @@ -52,9 +51,9 @@ export default class PreviewPane extends React.Component { this.previewEl = document.createElement('div'); this.iframeBody = ref.contentDocument.body; this.iframeBody.appendChild(this.previewEl); - this.renderPreview(this.props); + this.renderPreview(); } - } + }; render() { const { collection } = this.props; diff --git a/src/components/ScrollSync/ScrollSync.js b/src/components/ScrollSync/ScrollSync.js new file mode 100644 index 00000000..35ce2fc4 --- /dev/null +++ b/src/components/ScrollSync/ScrollSync.js @@ -0,0 +1,81 @@ +import React, { Component, PropTypes } from 'react'; +import { without } from 'lodash'; + +export default class ScrollSync extends Component { + + static propTypes = { + children: PropTypes.element.isRequired, + }; + + static childContextTypes = { + registerPane: PropTypes.func, + unregisterPane: PropTypes.func, + }; + + panes = []; + + getChildContext() { + return { + registerPane: this.registerPane, + unregisterPane: this.unregisterPane, + }; + } + + registerPane = node => { + if (!this.findPane(node)) { + this.addEvents(node); + this.panes.push(node); + } + }; + + unregisterPane = node => { + if (this.findPane(node)) { + this.removeEvents(node); + this.panes = without(this.panes, node); + } + }; + + addEvents = node => { + node.onscroll = this.handlePaneScroll.bind(this, node); + // node.addEventListener('scroll', this.handlePaneScroll, false) + }; + + removeEvents = node => { + node.onscroll = null; + // node.removeEventListener('scroll', this.handlePaneScroll, false) + }; + + findPane = node => { + return this.panes.find(p => p === node); + }; + + handlePaneScroll = node => { + // const node = evt.target + window.requestAnimationFrame(() => { + this.syncScrollPositions(node); + }); + }; + + syncScrollPositions = scrolledPane => { + const { scrollTop, scrollHeight, clientHeight } = scrolledPane; + this.panes.forEach(pane => { + /* For all panes beside the currently scrolling one */ + if (scrolledPane !== pane) { + /* Remove event listeners from the node that we'll manipulate */ + this.removeEvents(pane); + /* Calculate the actual pane height */ + const paneHeight = pane.scrollHeight - clientHeight; + /* Adjust the scrollTop position of it accordingly */ + pane.scrollTop = paneHeight * scrollTop / (scrollHeight - clientHeight); + /* Re-attach event listeners after we're done scrolling */ + window.requestAnimationFrame(() => { + this.addEvents(pane); + }); + } + }); + }; + + render() { + return React.Children.only(this.props.children); + } +} diff --git a/src/components/ScrollSync/ScrollSyncPane.js b/src/components/ScrollSync/ScrollSyncPane.js new file mode 100644 index 00000000..840edf59 --- /dev/null +++ b/src/components/ScrollSync/ScrollSyncPane.js @@ -0,0 +1,28 @@ +import { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; + +export default class ScrollSyncPane extends Component { + + static propTypes = { + children: PropTypes.node.isRequired, + attachTo: PropTypes.any + }; + + static contextTypes = { + registerPane: PropTypes.func.isRequired, + unregisterPane: PropTypes.func.isRequired, + }; + + componentDidMount() { + this.node = this.props.attachTo || ReactDOM.findDOMNode(this); + this.context.registerPane(this.node); + } + + componentWillUnmount() { + this.context.unregisterPane(this.node); + } + + render() { + return this.props.children; + } +} diff --git a/src/components/ScrollSync/index.js b/src/components/ScrollSync/index.js new file mode 100644 index 00000000..5a50c651 --- /dev/null +++ b/src/components/ScrollSync/index.js @@ -0,0 +1,2 @@ +export { default as ScrollSync } from './ScrollSync'; +export { default as ScrollSyncPane } from './ScrollSyncPane'; diff --git a/src/components/stories/ScrollSync.js b/src/components/stories/ScrollSync.js new file mode 100644 index 00000000..53a19b94 --- /dev/null +++ b/src/components/stories/ScrollSync.js @@ -0,0 +1,55 @@ +import React from 'react'; +import ScrollSync from '../ScrollSync/ScrollSync'; +import ScrollSyncPane from '../ScrollSync/ScrollSyncPane'; +import { storiesOf } from '@kadira/storybook'; + +const paneStyle = { + border: '1px solid green', + overflow: 'auto', +}; + +storiesOf('ScrollSync', module) + .add('Default', () => ( + +
+ +
+
+

Left Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+ + +
+
+

Right Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+ + +
+
+

Third Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+
+
+ )); diff --git a/src/components/stories/index.js b/src/components/stories/index.js index 30acb086..1e73d155 100644 --- a/src/components/stories/index.js +++ b/src/components/stories/index.js @@ -3,3 +3,4 @@ import './Icon'; import './Toast'; import './FindBar'; import './MarkupItReactRenderer'; +import './ScrollSync';