chore: hookifi supernova (#171)

This commit is contained in:
Christoffer Åström
2019-11-12 22:28:47 +01:00
committed by GitHub
parent bcf5f68540
commit 9b05f6c856
4 changed files with 125 additions and 204 deletions

View File

@@ -10,6 +10,8 @@ import Footer from './Footer';
import Supernova from './Supernova'; import Supernova from './Supernova';
import Placeholder from './Placeholder'; import Placeholder from './Placeholder';
import useRect from '../hooks/useRect';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
content: { content: {
position: 'relative', position: 'relative',
@@ -67,7 +69,7 @@ const Content = React.forwardRef(({ children, showError }, ref) => {
export default function Cell({ api, onInitial }) { export default function Cell({ api, onInitial }) {
const [, setChanged] = useState(0); const [, setChanged] = useState(0);
const [contentRef, contentRect, contentNode] = useRect();
const theme = useTheme(); const theme = useTheme();
useEffect(() => { useEffect(() => {
const onChanged = () => setChanged(Date.now()); const onChanged = () => setChanged(Date.now());
@@ -87,7 +89,6 @@ export default function Cell({ api, onInitial }) {
const SN = showRequirements(objectProps.sn, objectProps.layout) ? Requirements : Supernova; const SN = showRequirements(objectProps.sn, objectProps.layout) ? Requirements : Supernova;
const Comp = !objectProps.sn ? Placeholder : SN; const Comp = !objectProps.sn ? Placeholder : SN;
const showError = objectProps.error || objectProps.dataErrors.length; const showError = objectProps.error || objectProps.dataErrors.length;
return ( return (
<Paper style={{ height: '100%', position: 'relative' }} elevation={0} square className="nebulajs-cell"> <Paper style={{ height: '100%', position: 'relative' }} elevation={0} square className="nebulajs-cell">
<Grid container direction="column" spacing={0} style={{ height: '100%', padding: theme.spacing(1) }}> <Grid container direction="column" spacing={0} style={{ height: '100%', padding: theme.spacing(1) }}>
@@ -97,9 +98,9 @@ export default function Cell({ api, onInitial }) {
</Header> </Header>
</Grid> </Grid>
<Grid item xs> <Grid item xs>
<Content showError={showError}> <Content showError={showError} ref={contentRef}>
{showError ? ( {showError ? (
<CError err={objectProps.err} dataErrors={objectProps.dataErrors} /> <CError err={objectProps.err} dataErrors={objectProps.dataErrors} rect={contentRect} />
) : ( ) : (
<Comp <Comp
key={objectProps.layout.visualization} key={objectProps.layout.visualization}
@@ -107,6 +108,7 @@ export default function Cell({ api, onInitial }) {
snContext={userProps.context} snContext={userProps.context}
snOptions={userProps.options} snOptions={userProps.options}
layout={objectProps.layout} layout={objectProps.layout}
parentNode={contentNode}
/> />
)} )}
</Content> </Content>

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React, { useState, useEffect } from 'react';
import useRect from '../hooks/useRect';
const constrainElement = (el, d) => { const setStyle = (el, d) => {
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
if (d) { if (d) {
el.style.width = `${d.width}px`; el.style.width = `${d.width}px`;
@@ -18,211 +19,95 @@ const constrainElement = (el, d) => {
/* eslint-enable no-param-reassign */ /* eslint-enable no-param-reassign */
}; };
const scheduleRender = (props, prev, initial, contentElement) => { const constrainElement = ({ snNode, parentNode, sn, snContext, layout }) => {
if (prev) { const parentRect = parentNode.getBoundingClientRect();
prev.reject(); const r =
typeof snContext.logicalSize === 'function' ? snContext.logicalSize(layout, sn) : sn.logicalSize({ layout });
const logicalSize = r || undefined;
let width;
let height;
let left = 0;
let top = 0;
if (r) {
const parentRatio = parentRect.width / parentRect.height;
const rRatio = r.width / r.height;
if (parentRatio > rRatio) {
// parent is wider -> limit height
({ height } = parentRect);
width = height * rRatio;
left = (parentRect.width - width) / 2;
top = 0;
} else {
({ width } = parentRect);
height = width / rRatio;
left = 0;
top = (parentRect.height - height) / 2;
}
} }
const prom = {}; setStyle(snNode, r ? { left, top, width, height } : undefined);
return logicalSize;
const p = new Promise(resolve => {
const timeout = setTimeout(() => {
const parentRect = contentElement.parentElement.parentElement.getBoundingClientRect();
const r =
typeof props.snContext.logicalSize === 'function'
? props.snContext.logicalSize(props.layout, props.sn)
: props.sn.logicalSize({ layout: props.layout });
const logicalSize = r || undefined;
if (r) {
// const rect = that.element.getBoundingClientRect();
const parentRatio = parentRect.width / parentRect.height;
const rRatio = r.width / r.height;
let width;
let height;
let left = 0;
let top = 0;
if (parentRatio > rRatio) {
// parent is wider -> limit height
({ height } = parentRect);
width = height * rRatio;
left = (parentRect.width - width) / 2;
top = 0;
} else {
({ width } = parentRect);
height = width / rRatio;
left = 0;
top = (parentRect.height - height) / 2;
}
constrainElement(contentElement, {
top,
left,
width,
height,
});
} else {
constrainElement(contentElement);
}
initial.mount();
Promise.resolve(
props.sn.component.render({
layout: props.layout,
options: props.snOptions || {},
context: {
permissions: (props.snContext || {}).permissions,
theme: (props.snContext || {}).theme,
rtl: (props.snContext || {}).rtl,
localeInfo: (props.snContext || {}).localeInfo,
logicalSize,
},
})
)
.then(() => {
initial.rendered();
// props.sn.component.didUpdate(); // TODO - should check if component is in update stage
})
.then(resolve);
}, 0);
prom.reject = () => {
clearTimeout(timeout);
resolve();
};
});
prom.then = p.then;
return prom;
}; };
class Supernova extends React.Component { const Supernova = ({ sn, snOptions: options, snContext, layout, parentNode }) => {
constructor(props) { const { component } = sn;
super(props); const style = {
this.initial = { position: 'absolute',
mount: () => { top: 0,
this.props.sn.component.created({ bottom: 0,
options: this.props.snOptions, right: 0,
context: this.props.snContext, left: 0,
}); };
this.props.sn.component.mounted(this.contentElement); const [renderCnt, setRenderCnt] = useState(0);
this.initial.mount = () => {}; const [snRef, snRect, snNode] = useRect();
}, const [logicalSize, setLogicalSize] = useState({ width: 0, height: 0 });
rendered: () => {
if (this.props.snOptions && typeof this.props.snOptions.onInitialRender === 'function') { const render = () =>
this.props.snOptions.onInitialRender.call(null); component.render({
} layout,
this.initial.rendered = () => {}; options,
context: {
permissions: (snContext || {}).permissions,
theme: (snContext || {}).theme,
rtl: (snContext || {}).rtl,
localeInfo: (snContext || {}).localeInfo,
logicalSize,
}, },
});
// Mount / Unmount / ThemeChanged
useEffect(() => {
if (!snNode) return undefined;
setLogicalSize(constrainElement({ snNode, parentNode, sn, snContext, layout }));
component.created({ options, snContext });
component.mounted(snNode);
snContext.theme.on('changed', render);
return () => {
component.willUnmount();
snContext.theme.removeListener('changed', render);
}; };
} }, [snNode]);
componentDidMount() { // Render
let resizeObserver; useEffect(() => {
if (!snRect) return undefined;
this.dimensions = this.element.getBoundingClientRect(); if (renderCnt === 0) {
if (typeof options.onInitialRender === 'function') {
if (typeof ResizeObserver !== 'undefined') { options.onInitialRender.call(null);
resizeObserver = new ResizeObserver(() => {
const dims = this.element.getBoundingClientRect();
if (dims.width !== this.dimensions.width || dims.height !== this.dimensions.height) {
this.dimensions = dims;
this.setState({});
}
});
resizeObserver.observe(this.element);
}
const onThemeChanged = () => {
this.setState({});
};
this.props.snContext.theme.on('changed', onThemeChanged);
this.theme = this.props.snContext.theme;
this.onUnmount = () => {
this.onUnmount = null;
this.props.snContext.theme.removeListener('changed', onThemeChanged);
if (resizeObserver) {
resizeObserver.unobserve(this.element);
resizeObserver.disconnect();
resizeObserver = null;
} }
render();
this.props.sn.component.willUnmount(); setRenderCnt(renderCnt + 1);
if (this.next) { } else {
this.next.reject(); // Debounce render
} const handle = setTimeout(() => {
}; render();
setRenderCnt(renderCnt + 1);
this.next = scheduleRender( }, 100);
{ return () => clearTimeout(handle);
snOptions: this.props.snOptions,
snContext: this.props.snContext,
sn: this.props.sn,
layout: this.props.layout,
},
this.next,
this.initial,
this.contentElement
);
}
shouldComponentUpdate(nextProps) {
const update =
nextProps.sn &&
!(nextProps.layout && nextProps.layout.qSelectionInfo && nextProps.layout.qSelectionInfo.qInSelections);
if (!update) {
return false;
} }
return undefined;
return true; }, [snRect, layout]);
return <div ref={snRef} style={style} data-render-count={renderCnt} />;
// const should = nextProps.sn.component.shouldUpdate({ };
// layout: nextProps.layout,
// options: {},
// });
// return should;
}
componentDidUpdate() {
this.next = scheduleRender(this.props, this.next, this.initial, this.contentElement);
}
componentWillUnmount() {
this.onUnmount();
}
render() {
const style = {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
};
return (
<div
style={style}
ref={element => {
this.element = element;
}}
>
<div
style={{
position: 'absolute',
}}
ref={element => {
this.contentElement = element;
}}
/>
</div>
);
}
}
export default Supernova; export default Supernova;

View File

@@ -0,0 +1,33 @@
import { useState, useCallback, useLayoutEffect } from 'react';
export default function useRect() {
const [node, setNode] = useState();
const [rect, setRect] = useState();
const callbackRef = useCallback(ref => {
if (!ref) {
return;
}
setNode(ref);
}, []);
const handleResize = () => {
const { left, top, width, height } = node.getBoundingClientRect();
setRect({ left, top, width, height });
};
useLayoutEffect(() => {
if (!node) return undefined;
if (typeof ResizeObserver === 'function') {
let resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(node);
return () => {
resizeObserver.unobserve(node);
resizeObserver.disconnect(node);
resizeObserver = null;
};
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [node]);
return [callbackRef, rect, node];
}

View File

@@ -109,6 +109,7 @@ const config = isEsm => {
react: [ react: [
'useState', 'useState',
'useEffect', 'useEffect',
'useLayoutEffect',
'useRef', 'useRef',
'useContext', 'useContext',
'useCallback', 'useCallback',