mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
chore: hookifi supernova (#171)
This commit is contained in:
committed by
GitHub
parent
bcf5f68540
commit
9b05f6c856
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
33
apis/nucleus/src/hooks/useRect.js
Normal file
33
apis/nucleus/src/hooks/useRect.js
Normal 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];
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ const config = isEsm => {
|
|||||||
react: [
|
react: [
|
||||||
'useState',
|
'useState',
|
||||||
'useEffect',
|
'useEffect',
|
||||||
|
'useLayoutEffect',
|
||||||
'useRef',
|
'useRef',
|
||||||
'useContext',
|
'useContext',
|
||||||
'useCallback',
|
'useCallback',
|
||||||
|
|||||||
Reference in New Issue
Block a user