Implement ScrollSync component for sync scroll between containers

This commit is contained in:
Andrey Okonetchnikov 2016-10-04 17:58:26 +02:00
parent ca34def49e
commit b95bb595f7
7 changed files with 210 additions and 63 deletions

View File

@ -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 (
<div className={styles.root}>
export default function EntryEditor(
{
collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist
}) {
return (
<div className={styles.root}>
<ScrollSync>
<div className={styles.container}>
<div
className={styles.controlPane}
onScroll={this.handleControlPaneScroll}
>
<ControlPane
collection={collection}
entry={entry}
getMedia={getMedia}
onChange={onChange}
onAddMedia={onAddMedia}
onRemoveMedia={onRemoveMedia}
/>
</div>
<ScrollSyncPane>
<div className={styles.controlPane}>
<ControlPane
collection={collection}
entry={entry}
getMedia={getMedia}
onChange={onChange}
onAddMedia={onAddMedia}
onRemoveMedia={onRemoveMedia}
/>
</div>
</ScrollSyncPane>
<div className={styles.previewPane}>
<PreviewPane
collection={collection}
entry={entry}
getMedia={getMedia}
scrollTop={scrollTop}
scrollHeight={scrollHeight}
offsetHeight={offsetHeight}
/>
</div>
</div>
<div className={styles.footer}>
<button onClick={onPersist}>Save</button>
</div>
</ScrollSync>
<div className={styles.footer}>
<button onClick={onPersist}>Save</button>
</div>
);
}
</div>
);
}
EntryEditor.propTypes = {

View File

@ -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,
<ScrollSyncPane attachTo={this.iframeBody}>
{React.createElement(component, previewProps)}
</ScrollSyncPane>
, 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;

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,2 @@
export { default as ScrollSync } from './ScrollSync';
export { default as ScrollSyncPane } from './ScrollSyncPane';

View File

@ -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', () => (
<ScrollSync>
<div style={{ display: 'flex', position: 'relative', height: 500 }}>
<ScrollSyncPane>
<div style={paneStyle}>
<section style={{ height: 5000 }}>
<h1>Left Pane Content</h1>
<p>
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.
</p>
</section>
</div>
</ScrollSyncPane>
<ScrollSyncPane>
<div style={paneStyle}>
<section style={{ height: 10000 }}>
<h1>Right Pane Content</h1>
<p>
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.
</p>
</section>
</div>
</ScrollSyncPane>
<ScrollSyncPane>
<div style={paneStyle}>
<section style={{ height: 2000 }}>
<h1>Third Pane Content</h1>
<p>
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.
</p>
</section>
</div>
</ScrollSyncPane>
</div>
</ScrollSync>
));

View File

@ -3,3 +3,4 @@ import './Icon';
import './Toast';
import './FindBar';
import './MarkupItReactRenderer';
import './ScrollSync';