UNPKG

react-virtualized-auto-sizer

Version:

Standalone version of the AutoSizer component from react-virtualized

403 lines (388 loc) 15.9 kB
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 };