react-use-motion-measure
Version:
measure view bounds
243 lines (224 loc) • 7.79 kB
JavaScript
;
var react = require('react');
var createDebounce = require('debounce');
var framerMotion = require('framer-motion');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var createDebounce__default = /*#__PURE__*/_interopDefaultLegacy(createDebounce);
function useMotionMeasure(_temp) {
var _ref = _temp === void 0 ? {
debounce: 0,
scroll: false,
offsetSize: false
} : _temp,
debounce = _ref.debounce,
scroll = _ref.scroll,
polyfill = _ref.polyfill,
offsetSize = _ref.offsetSize;
var ResizeObserver = polyfill || (typeof window === 'undefined' ? function ResizeObserver() {} : window.ResizeObserver);
if (!ResizeObserver) {
throw new Error('This browser does not support ResizeObserver out of the box. See: https://github.com/OlegWock/react-use-motion-measure/#resize-observer-polyfills');
}
var left = framerMotion.useMotionValue(0);
var top = framerMotion.useMotionValue(0);
var width = framerMotion.useMotionValue(0);
var height = framerMotion.useMotionValue(0);
var bottom = framerMotion.useMotionValue(0);
var right = framerMotion.useMotionValue(0);
var x = framerMotion.useMotionValue(0);
var y = framerMotion.useMotionValue(0);
// keep all state in a ref
var state = react.useRef({
element: null,
measuredAtLeastOnce: false,
scrollContainers: null,
resizeObserver: null,
lastBounds: getLastBounds()
});
// set actual debounce values early, so effects know if they should react accordingly
var scrollDebounce = debounce ? typeof debounce === 'number' ? debounce : debounce.scroll : null;
var resizeDebounce = debounce ? typeof debounce === 'number' ? debounce : debounce.resize : null;
// make sure to update state only as long as the component is truly mounted
var mounted = react.useRef(false);
react.useEffect(function () {
mounted.current = true;
return function () {
return void (mounted.current = false);
};
});
// memoize handlers, so event-listeners know when they should update
var _useMemo = react.useMemo(function () {
var callback = function callback() {
if (!state.current.element) return;
var _ref2 = state.current.element.getBoundingClientRect(),
left = _ref2.left,
top = _ref2.top,
width = _ref2.width,
height = _ref2.height,
bottom = _ref2.bottom,
right = _ref2.right,
x = _ref2.x,
y = _ref2.y;
var size = {
left: left,
top: top,
width: width,
height: height,
bottom: bottom,
right: right,
x: x,
y: y
};
if (state.current.element instanceof HTMLElement && offsetSize) {
size.height = state.current.element.offsetHeight;
size.width = state.current.element.offsetWidth;
}
Object.freeze(size);
if (mounted.current && !areBoundsEqual(state.current.lastBounds, size)) {
if (state.current.measuredAtLeastOnce) {
set(state.current.lastBounds = size);
} else {
jump(state.current.lastBounds = size);
}
}
};
return [callback, resizeDebounce ? createDebounce__default["default"](callback, resizeDebounce) : callback, scrollDebounce ? createDebounce__default["default"](callback, scrollDebounce) : callback];
}, [set, jump, offsetSize, scrollDebounce, resizeDebounce]),
forceRefresh = _useMemo[0],
resizeChange = _useMemo[1],
scrollChange = _useMemo[2];
// cleanup current scroll-listeners / observers
function removeListeners() {
if (state.current.scrollContainers) {
state.current.scrollContainers.forEach(function (element) {
return element.removeEventListener('scroll', scrollChange, true);
});
state.current.scrollContainers = null;
}
if (state.current.resizeObserver) {
state.current.resizeObserver.disconnect();
state.current.resizeObserver = null;
}
}
// add scroll-listeners / observers
function addListeners() {
if (!state.current.element) return;
state.current.resizeObserver = new ResizeObserver(scrollChange);
state.current.resizeObserver.observe(state.current.element);
if (scroll && state.current.scrollContainers) {
state.current.scrollContainers.forEach(function (scrollContainer) {
return scrollContainer.addEventListener('scroll', scrollChange, {
capture: true,
passive: true
});
});
}
}
function getLastBounds() {
return {
left: left.get(),
top: top.get(),
width: width.get(),
height: height.get(),
bottom: bottom.get(),
right: right.get(),
x: x.get(),
y: y.get()
};
}
function set(bounds) {
left.set(bounds.left);
top.set(bounds.top);
width.set(bounds.width);
height.set(bounds.height);
bottom.set(bounds.bottom);
right.set(bounds.right);
x.set(bounds.x);
y.set(bounds.y);
}
function jump(bounds) {
left.jump(bounds.left);
top.jump(bounds.top);
width.jump(bounds.width);
height.jump(bounds.height);
bottom.jump(bounds.bottom);
right.jump(bounds.right);
x.jump(bounds.x);
y.jump(bounds.y);
}
// the ref we expose to the user
var ref = function ref(node) {
if (!node || node === state.current.element) return;
removeListeners();
state.current.element = node;
state.current.measuredAtLeastOnce = false;
state.current.scrollContainers = findScrollContainers(node);
addListeners();
};
// add general event listeners
useOnWindowScroll(scrollChange, Boolean(scroll));
useOnWindowResize(resizeChange);
// respond to changes that are relevant for the listeners
react.useEffect(function () {
removeListeners();
addListeners();
}, [scroll, scrollChange, resizeChange]);
// remove all listeners when the components unmounts
react.useEffect(function () {
return removeListeners;
}, []);
return [ref, {
left: left,
top: top,
width: width,
height: height,
bottom: bottom,
right: right,
x: x,
y: y
}, forceRefresh];
}
// Adds native resize listener to window
function useOnWindowResize(onWindowResize) {
react.useEffect(function () {
var cb = onWindowResize;
window.addEventListener('resize', cb);
return function () {
return void window.removeEventListener('resize', cb);
};
}, [onWindowResize]);
}
function useOnWindowScroll(onScroll, enabled) {
react.useEffect(function () {
if (enabled) {
var _cb = onScroll;
window.addEventListener('scroll', _cb, {
capture: true,
passive: true
});
return function () {
return void window.removeEventListener('scroll', _cb, true);
};
}
}, [onScroll, enabled]);
}
// Returns a list of scroll offsets
function findScrollContainers(element) {
var result = [];
if (!element || element === document.body) return result;
var _window$getComputedSt = window.getComputedStyle(element),
overflow = _window$getComputedSt.overflow,
overflowX = _window$getComputedSt.overflowX,
overflowY = _window$getComputedSt.overflowY;
if ([overflow, overflowX, overflowY].some(function (prop) {
return prop === 'auto' || prop === 'scroll';
})) result.push(element);
return [].concat(result, findScrollContainers(element.parentElement));
}
// Checks if element boundaries are equal
var keys = ['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height'];
var areBoundsEqual = function areBoundsEqual(a, b) {
return keys.every(function (key) {
return a[key] === b[key];
});
};
module.exports = useMotionMeasure;