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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { ScrollSync, ScrollSyncPane } from '../ScrollSync';
|
||||||
import ControlPane from '../ControlPanel/ControlPane';
|
import ControlPane from '../ControlPanel/ControlPane';
|
||||||
import PreviewPane from '../PreviewPane/PreviewPane';
|
import PreviewPane from '../PreviewPane/PreviewPane';
|
||||||
import styles from './EntryEditor.css';
|
import styles from './EntryEditor.css';
|
||||||
|
|
||||||
export default class EntryEditor extends Component {
|
export default function EntryEditor(
|
||||||
|
{
|
||||||
state = {
|
collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist
|
||||||
scrollTop: 0,
|
}) {
|
||||||
scrollHeight: 0,
|
return (
|
||||||
offsetHeight: 0,
|
<div className={styles.root}>
|
||||||
}
|
<ScrollSync>
|
||||||
|
|
||||||
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}>
|
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div
|
<ScrollSyncPane>
|
||||||
className={styles.controlPane}
|
<div className={styles.controlPane}>
|
||||||
onScroll={this.handleControlPaneScroll}
|
<ControlPane
|
||||||
>
|
collection={collection}
|
||||||
<ControlPane
|
entry={entry}
|
||||||
collection={collection}
|
getMedia={getMedia}
|
||||||
entry={entry}
|
onChange={onChange}
|
||||||
getMedia={getMedia}
|
onAddMedia={onAddMedia}
|
||||||
onChange={onChange}
|
onRemoveMedia={onRemoveMedia}
|
||||||
onAddMedia={onAddMedia}
|
/>
|
||||||
onRemoveMedia={onRemoveMedia}
|
</div>
|
||||||
/>
|
</ScrollSyncPane>
|
||||||
</div>
|
|
||||||
<div className={styles.previewPane}>
|
<div className={styles.previewPane}>
|
||||||
<PreviewPane
|
<PreviewPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
getMedia={getMedia}
|
getMedia={getMedia}
|
||||||
scrollTop={scrollTop}
|
|
||||||
scrollHeight={scrollHeight}
|
|
||||||
offsetHeight={offsetHeight}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.footer}>
|
</ScrollSync>
|
||||||
<button onClick={onPersist}>Save</button>
|
<div className={styles.footer}>
|
||||||
</div>
|
<button onClick={onPersist}>Save</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryEditor.propTypes = {
|
EntryEditor.propTypes = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { render } from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { ScrollSyncPane } from '../ScrollSync';
|
||||||
import registry from '../../lib/registry';
|
import registry from '../../lib/registry';
|
||||||
import { resolveWidget } from '../Widgets';
|
import { resolveWidget } from '../Widgets';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
@ -8,16 +9,8 @@ import styles from './PreviewPane.css';
|
|||||||
|
|
||||||
export default class PreviewPane extends React.Component {
|
export default class PreviewPane extends React.Component {
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate() {
|
||||||
// Update scroll position of the iframe
|
this.renderPreview();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
widgetFor = name => {
|
widgetFor = name => {
|
||||||
@ -30,15 +23,21 @@ export default class PreviewPane extends React.Component {
|
|||||||
field,
|
field,
|
||||||
getMedia,
|
getMedia,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
renderPreview(props) {
|
renderPreview() {
|
||||||
const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview;
|
const component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview;
|
||||||
const previewProps = {
|
const previewProps = {
|
||||||
...props,
|
...this.props,
|
||||||
widgetFor: this.widgetFor
|
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 => {
|
handleIframeRef = ref => {
|
||||||
@ -52,9 +51,9 @@ export default class PreviewPane extends React.Component {
|
|||||||
this.previewEl = document.createElement('div');
|
this.previewEl = document.createElement('div');
|
||||||
this.iframeBody = ref.contentDocument.body;
|
this.iframeBody = ref.contentDocument.body;
|
||||||
this.iframeBody.appendChild(this.previewEl);
|
this.iframeBody.appendChild(this.previewEl);
|
||||||
this.renderPreview(this.props);
|
this.renderPreview();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collection } = this.props;
|
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 './Toast';
|
||||||
import './FindBar';
|
import './FindBar';
|
||||||
import './MarkupItReactRenderer';
|
import './MarkupItReactRenderer';
|
||||||
|
import './ScrollSync';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user