Last active
September 29, 2018 19:13
-
-
Save bruce965/523707410ae82af5eaff7f7ea97195c6 to your computer and use it in GitHub Desktop.
TypeScript Element Resized Sensor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import classes from './style.less'; | |
const CLASSNAME_RESIZE_SENSOR = classes['resize-sensor']; | |
export interface ResizeSensorOptions { | |
element: HTMLElement; | |
onResize(): void; | |
} | |
/** | |
* Detects when the size of an `HTMLElement` changes. | |
*/ | |
export class ResizeSensor | |
{ | |
private readonly _sensor: HTMLElement; | |
constructor(options: ResizeSensorOptions) { | |
const element = options.element; | |
const handler = options.onResize; | |
const computedStyle = getComputedStyle(element); | |
if (computedStyle.position == 'static') | |
console.warn(new Error("ResizeSensor does not support 'position: static' elements")); | |
else if (computedStyle.display == 'inline') | |
console.warn(new Error("ResizeSensor does not support 'display: inline' elements")); | |
// # How does it work? | |
// | |
// Inspired by https://github.com/sdecima/javascript-detect-element-resize | |
// | |
// Through the `reset()` method, we keep `growSensor` scrollable by a single pixel | |
// and always scrolled to the end. | |
// When it grows, it becomes no longer scrollable, and the 'scroll' event triggers. | |
// | |
// `shrinkSensor` contains a child which is twice its size, so that `shrinkSensor` | |
// can always scroll by it's whole visible size. | |
// Through the `reset()` method, we keep it scrolled to the end. | |
// When it shrinks, it is forced to scroll back, and the 'scroll' event triggers. | |
// | |
// The size of `growSensor` and `shrinkSensor` is set in percentage relative to | |
// the size of `element`, therefore, when a 'scroll' event triggers, we know that | |
// `element` must have changed in size. | |
// | |
// ``` | |
// - element | |
// - this._sensor | |
// - growSensor | |
// - growSensorChild | |
// - shrinkSensor | |
// - shrinkSensorChild | |
// ``` | |
this._sensor = document.createElement('div'); | |
this._sensor.classList.add(CLASSNAME_RESIZE_SENSOR); | |
const growSensor = document.createElement('div'); | |
const growSensorChild = document.createElement('div'); | |
growSensor.appendChild(growSensorChild); | |
this._sensor.appendChild(growSensor); | |
const shrinkSensor = document.createElement('div'); | |
const shrinkSensorChild = document.createElement('div'); | |
shrinkSensor.appendChild(shrinkSensorChild); | |
this._sensor.appendChild(shrinkSensor); | |
let animationFrameId: number|undefined; | |
let lastWidth: number; | |
let lastHeight: number; | |
const reset = () => { | |
growSensorChild.style.width = `${growSensor.offsetWidth}px`; | |
growSensorChild.style.height = `${growSensor.offsetHeight}px`; | |
growSensor.scrollLeft = growSensor.scrollWidth; | |
growSensor.scrollTop = growSensor.scrollHeight; | |
shrinkSensor.scrollLeft = shrinkSensor.scrollWidth; | |
shrinkSensor.scrollTop = shrinkSensor.scrollHeight; | |
lastWidth = element.clientWidth; | |
lastHeight = element.clientHeight; | |
}; | |
const listenForScroll = () => { | |
// do nothing if already triggered within a single animation frame | |
if (animationFrameId) | |
return; | |
// on next animation frame, reset sensor's state and call the handler | |
animationFrameId = window.requestAnimationFrame(() => { | |
animationFrameId = undefined; | |
// sometimes 'scroll' events might be triggered even without a resize, | |
// we filter out these spurious events by checking for size change | |
const changed = ( | |
element.clientWidth != lastWidth || | |
element.clientHeight != lastHeight | |
); | |
reset(); | |
if (changed) | |
handler(); | |
}); | |
}; | |
growSensor.addEventListener('scroll', listenForScroll); | |
shrinkSensor.addEventListener('scroll', listenForScroll); | |
element.appendChild(this._sensor); | |
// reset on next animation frame in case element is not fully styled yet | |
window.requestAnimationFrame(() => reset()); | |
} | |
stop(): void { | |
this._sensor.remove(); | |
} | |
} | |
export default ResizeSensor; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.resize-sensor { | |
visibility: hidden; | |
opacity: 0; // HACK: Chrome wontfix https://code.google.com/p/chromium/issues/detail?id=286360 | |
* { | |
// NOTE: some of the attributes are useless for some of the targeted elements, | |
// but writing it as a single selector makes the CSS smaller. | |
// sensors and their children must be absolutely positioned | |
position: absolute; | |
// `growSensor` and `shrinkSensor` must be sized relative to parent (size as percentage), | |
// `growSensorChild` must be at least twice the size of `growSensor` | |
width: 200%; | |
height: 200%; | |
// `growSensor` and `shrinkSensor` must be scrollable | |
overflow: scroll; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment