react-virtualized-auto-sizer
Version:
Standalone version of the AutoSizer component from react-virtualized
403 lines (388 loc) • 15.9 kB
JavaScript
import { Component, createElement } from 'react';
/**
* Detect Element Resize.
* https://github.com/sdecima/javascript-detect-element-resize
* Sebastian Decima
*
* Forked from version 0.5.3; includes the following modifications:
* 1) Guard against unsafe 'window' and 'document' references (to support SSR).
* 2) Defer initialization code via a top-level function wrapper (to support SSR).
* 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children.
* 4) Add nonce for style element.
* 5) Use 'export' statement over 'module.exports' assignment
**/
// Check `document` and `window` in case of server-side rendering
let windowObject;
if (typeof window !== "undefined") {
windowObject = window;
// eslint-disable-next-line no-restricted-globals
} else if (typeof self !== "undefined") {
// eslint-disable-next-line no-restricted-globals
windowObject = self;
} else {
windowObject = global;
}
let cancelFrame = null;
let requestFrame = null;
const TIMEOUT_DURATION = 20;
const clearTimeoutFn = windowObject.clearTimeout;
const setTimeoutFn = windowObject.setTimeout;
const cancelAnimationFrameFn = windowObject.cancelAnimationFrame || windowObject.mozCancelAnimationFrame || windowObject.webkitCancelAnimationFrame;
const requestAnimationFrameFn = windowObject.requestAnimationFrame || windowObject.mozRequestAnimationFrame || windowObject.webkitRequestAnimationFrame;
if (cancelAnimationFrameFn == null || requestAnimationFrameFn == null) {
// For environments that don't support animation frame,
// fallback to a setTimeout based approach.
cancelFrame = clearTimeoutFn;
requestFrame = function requestAnimationFrameViaSetTimeout(callback) {
return setTimeoutFn(callback, TIMEOUT_DURATION);
};
} else {
// Counter intuitively, environments that support animation frames can be trickier.
// Chrome's "Throttle non-visible cross-origin iframes" flag can prevent rAFs from being called.
// In this case, we should fallback to a setTimeout() implementation.
cancelFrame = function cancelFrame([animationFrameID, timeoutID]) {
cancelAnimationFrameFn(animationFrameID);
clearTimeoutFn(timeoutID);
};
requestFrame = function requestAnimationFrameWithSetTimeoutFallback(callback) {
const animationFrameID = requestAnimationFrameFn(function animationFrameCallback() {
clearTimeoutFn(timeoutID);
callback();
});
const timeoutID = setTimeoutFn(function timeoutCallback() {
cancelAnimationFrameFn(animationFrameID);
callback();
}, TIMEOUT_DURATION);
return [animationFrameID, timeoutID];
};
}
function createDetectElementResize(nonce) {
let animationKeyframes;
let animationName;
let animationStartEvent;
let animationStyle;
let checkTriggers;
let resetTriggers;
let scrollListener;
const attachEvent = typeof document !== "undefined" && document.attachEvent;
if (!attachEvent) {
resetTriggers = function (element) {
const triggers = element.__resizeTriggers__,
expand = triggers.firstElementChild,
contract = triggers.lastElementChild,
expandChild = expand.firstElementChild;
contract.scrollLeft = contract.scrollWidth;
contract.scrollTop = contract.scrollHeight;
expandChild.style.width = expand.offsetWidth + 1 + "px";
expandChild.style.height = expand.offsetHeight + 1 + "px";
expand.scrollLeft = expand.scrollWidth;
expand.scrollTop = expand.scrollHeight;
};
checkTriggers = function (element) {
return element.offsetWidth !== element.__resizeLast__.width || element.offsetHeight !== element.__resizeLast__.height;
};
scrollListener = function (e) {
// Don't measure (which forces) reflow for scrolls that happen inside of children!
if (e.target.className && typeof e.target.className.indexOf === "function" && e.target.className.indexOf("contract-trigger") < 0 && e.target.className.indexOf("expand-trigger") < 0) {
return;
}
const element = this;
resetTriggers(this);
if (this.__resizeRAF__) {
cancelFrame(this.__resizeRAF__);
}
this.__resizeRAF__ = requestFrame(function animationFrame() {
if (checkTriggers(element)) {
element.__resizeLast__.width = element.offsetWidth;
element.__resizeLast__.height = element.offsetHeight;
element.__resizeListeners__.forEach(function forEachResizeListener(fn) {
fn.call(element, e);
});
}
});
};
/* Detect CSS Animations support to detect element display/re-attach */
let animation = false;
let keyframeprefix = "";
animationStartEvent = "animationstart";
const domPrefixes = "Webkit Moz O ms".split(" ");
let startEvents = "webkitAnimationStart animationstart oAnimationStart MSAnimationStart".split(" ");
let pfx = "";
{
const elm = document.createElement("fakeelement");
if (elm.style.animationName !== undefined) {
animation = true;
}
if (animation === false) {
for (let i = 0; i < domPrefixes.length; i++) {
if (elm.style[domPrefixes[i] + "AnimationName"] !== undefined) {
pfx = domPrefixes[i];
keyframeprefix = "-" + pfx.toLowerCase() + "-";
animationStartEvent = startEvents[i];
animation = true;
break;
}
}
}
}
animationName = "resizeanim";
animationKeyframes = "@" + keyframeprefix + "keyframes " + animationName + " { from { opacity: 0; } to { opacity: 0; } } ";
animationStyle = keyframeprefix + "animation: 1ms " + animationName + "; ";
}
const createStyles = function (doc) {
if (!doc.getElementById("detectElementResize")) {
//opacity:0 works around a chrome bug https://code.google.com/p/chromium/issues/detail?id=286360
const css = (animationKeyframes ? animationKeyframes : "") + ".resize-triggers { " + (animationStyle ? animationStyle : "") + "visibility: hidden; opacity: 0; } " + '.resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }',
head = doc.head || doc.getElementsByTagName("head")[0],
style = doc.createElement("style");
style.id = "detectElementResize";
style.type = "text/css";
if (nonce != null) {
style.setAttribute("nonce", nonce);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(doc.createTextNode(css));
}
head.appendChild(style);
}
};
const addResizeListener = function (element, fn) {
if (attachEvent) {
element.attachEvent("onresize", fn);
} else {
if (!element.__resizeTriggers__) {
const doc = element.ownerDocument;
const elementStyle = windowObject.getComputedStyle(element);
if (elementStyle && elementStyle.position === "static") {
element.style.position = "relative";
}
createStyles(doc);
element.__resizeLast__ = {};
element.__resizeListeners__ = [];
(element.__resizeTriggers__ = doc.createElement("div")).className = "resize-triggers";
const expandTrigger = doc.createElement("div");
expandTrigger.className = "expand-trigger";
expandTrigger.appendChild(doc.createElement("div"));
const contractTrigger = doc.createElement("div");
contractTrigger.className = "contract-trigger";
element.__resizeTriggers__.appendChild(expandTrigger);
element.__resizeTriggers__.appendChild(contractTrigger);
element.appendChild(element.__resizeTriggers__);
resetTriggers(element);
element.addEventListener("scroll", scrollListener, true);
/* Listen for a css animation to detect element display/re-attach */
if (animationStartEvent) {
element.__resizeTriggers__.__animationListener__ = function animationListener(e) {
if (e.animationName === animationName) {
resetTriggers(element);
}
};
element.__resizeTriggers__.addEventListener(animationStartEvent, element.__resizeTriggers__.__animationListener__);
}
}
element.__resizeListeners__.push(fn);
}
};
const removeResizeListener = function (element, fn) {
if (attachEvent) {
element.detachEvent("onresize", fn);
} else {
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1);
if (!element.__resizeListeners__.length) {
element.removeEventListener("scroll", scrollListener, true);
if (element.__resizeTriggers__.__animationListener__) {
element.__resizeTriggers__.removeEventListener(animationStartEvent, element.__resizeTriggers__.__animationListener__);
element.__resizeTriggers__.__animationListener__ = null;
}
try {
element.__resizeTriggers__ = !element.removeChild(element.__resizeTriggers__);
} catch (e) {
// Preact compat; see developit/preact-compat/issues/228
}
}
}
};
return {
addResizeListener,
removeResizeListener
};
}
class AutoSizer extends Component {
constructor(...args) {
super(...args);
this.state = {
height: this.props.defaultHeight || 0,
width: this.props.defaultWidth || 0
};
this._autoSizer = null;
this._detectElementResize = null;
this._didLogDeprecationWarning = false;
this._parentNode = null;
this._resizeObserver = null;
this._timeoutId = null;
this._onResize = () => {
this._timeoutId = null;
const {
disableHeight,
disableWidth,
onResize
} = this.props;
if (this._parentNode) {
// Guard against AutoSizer component being removed from the DOM immediately after being added.
// This can result in invalid style values which can result in NaN values if we don't handle them.
// See issue #150 for more context.
const style = window.getComputedStyle(this._parentNode) || {};
const paddingLeft = parseFloat(style.paddingLeft || "0");
const paddingRight = parseFloat(style.paddingRight || "0");
const paddingTop = parseFloat(style.paddingTop || "0");
const paddingBottom = parseFloat(style.paddingBottom || "0");
const rect = this._parentNode.getBoundingClientRect();
const height = rect.height - paddingTop - paddingBottom;
const width = rect.width - paddingLeft - paddingRight;
if (!disableHeight && this.state.height !== height || !disableWidth && this.state.width !== width) {
this.setState({
height,
width
});
const maybeLogDeprecationWarning = () => {
if (!this._didLogDeprecationWarning) {
this._didLogDeprecationWarning = true;
console.warn("scaledWidth and scaledHeight parameters have been deprecated; use width and height instead");
}
};
if (typeof onResize === "function") {
onResize({
height,
width,
// TODO Remove these params in the next major release
get scaledHeight() {
maybeLogDeprecationWarning();
return height;
},
get scaledWidth() {
maybeLogDeprecationWarning();
return width;
}
});
}
}
}
};
this._setRef = autoSizer => {
this._autoSizer = autoSizer;
};
}
componentDidMount() {
const {
nonce
} = this.props;
const parentNode = this._autoSizer ? this._autoSizer.parentNode : null;
if (parentNode != null && parentNode.ownerDocument && parentNode.ownerDocument.defaultView && parentNode instanceof parentNode.ownerDocument.defaultView.HTMLElement) {
// Delay access of parentNode until mount.
// This handles edge-cases where the component has already been unmounted before its ref has been set,
// As well as libraries like react-lite which have a slightly different lifecycle.
this._parentNode = parentNode;
// Use ResizeObserver from the same context where parentNode (which we will observe) was defined
// Using just global can result into onResize events not being emitted in cases with multiple realms
const ResizeObserverInstance = parentNode.ownerDocument.defaultView.ResizeObserver;
if (ResizeObserverInstance != null) {
this._resizeObserver = new ResizeObserverInstance(() => {
// Guard against "ResizeObserver loop limit exceeded" error;
// could be triggered if the state update causes the ResizeObserver handler to run long.
// See https://github.com/bvaughn/react-virtualized-auto-sizer/issues/55
this._timeoutId = setTimeout(this._onResize, 0);
});
this._resizeObserver.observe(parentNode);
} else {
// Defer requiring resize handler in order to support server-side rendering.
// See issue #41
this._detectElementResize = createDetectElementResize(nonce);
this._detectElementResize.addResizeListener(parentNode, this._onResize);
}
this._onResize();
}
}
componentWillUnmount() {
if (this._parentNode) {
if (this._detectElementResize) {
this._detectElementResize.removeResizeListener(this._parentNode, this._onResize);
}
if (this._timeoutId !== null) {
clearTimeout(this._timeoutId);
}
if (this._resizeObserver) {
this._resizeObserver.disconnect();
}
}
}
render() {
const {
children,
defaultHeight,
defaultWidth,
disableHeight = false,
disableWidth = false,
doNotBailOutOnEmptyChildren = false,
nonce,
onResize,
style = {},
tagName = "div",
...rest
} = this.props;
const {
height,
width
} = this.state;
// Outer div should not force width/height since that may prevent containers from shrinking.
// Inner component should overflow and use calculated width/height.
// See issue #68 for more information.
const outerStyle = {
overflow: "visible"
};
const childParams = {};
// Avoid rendering children before the initial measurements have been collected.
// At best this would just be wasting cycles.
let bailoutOnChildren = false;
if (!disableHeight) {
if (height === 0) {
bailoutOnChildren = true;
}
outerStyle.height = 0;
childParams.height = height;
// TODO Remove this in the next major release
childParams.scaledHeight = height;
}
if (!disableWidth) {
if (width === 0) {
bailoutOnChildren = true;
}
outerStyle.width = 0;
childParams.width = width;
// TODO Remove this in the next major release
childParams.scaledWidth = width;
}
if (doNotBailOutOnEmptyChildren) {
bailoutOnChildren = false;
}
return createElement(tagName, {
ref: this._setRef,
style: {
...outerStyle,
...style
},
...rest
}, !bailoutOnChildren && children(childParams));
}
}
function isHeightAndWidthProps(props) {
return props && props.disableHeight !== true && props.disableWidth !== true;
}
function isHeightOnlyProps(props) {
return props && props.disableHeight !== true && props.disableWidth === true;
}
function isWidthOnlyProps(props) {
return props && props.disableHeight === true && props.disableWidth !== true;
}
export { AutoSizer as default, isHeightAndWidthProps, isHeightOnlyProps, isWidthOnlyProps };