Implement ScrollSync component for sync scroll between containers
This commit is contained in:
parent
ca34def49e
commit
b95bb595f7
@ -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 = {
|
||||
|
@ -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;
|
||||
|
81
src/components/ScrollSync/ScrollSync.js
Normal file
81
src/components/ScrollSync/ScrollSync.js
Normal 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);
|
||||
}
|
||||
}
|
28
src/components/ScrollSync/ScrollSyncPane.js
Normal file
28
src/components/ScrollSync/ScrollSyncPane.js
Normal 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;
|
||||
}
|
||||
}
|
2
src/components/ScrollSync/index.js
Normal file
2
src/components/ScrollSync/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as ScrollSync } from './ScrollSync';
|
||||
export { default as ScrollSyncPane } from './ScrollSyncPane';
|
55
src/components/stories/ScrollSync.js
Normal file
55
src/components/stories/ScrollSync.js
Normal 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>
|
||||
));
|
@ -3,3 +3,4 @@ import './Icon';
|
||||
import './Toast';
|
||||
import './FindBar';
|
||||
import './MarkupItReactRenderer';
|
||||
import './ScrollSync';
|
||||
|
Loading…
x
Reference in New Issue
Block a user