Skip to content

Instantly share code, notes, and snippets.

@bruce965
Last active September 29, 2018 19:13
Show Gist options
  • Save bruce965/523707410ae82af5eaff7f7ea97195c6 to your computer and use it in GitHub Desktop.
Save bruce965/523707410ae82af5eaff7f7ea97195c6 to your computer and use it in GitHub Desktop.
TypeScript Element Resized Sensor
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;
.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