window.devicePixelRatio change listener - javascript

window.devicePixelRatio will return 1 or 2 depending on if I'm using my retina monitor or standard. If I drag the window between the two monitors, this property will change. Is there a way I can have a listener fire when the change occurs?

You can listen to a media query with matchMedia that will tell you when the devicePixelRatio goes past a certain barrier (unfortunately not for arbitrary scale changes).
e.g:
window.matchMedia('screen and (min-resolution: 2dppx)')
.addEventListener("change", function(e) {
if (e.matches) {
/* devicePixelRatio >= 2 */
} else {
/* devicePixelRatio < 2 */
}
});
The listener will be called when you drag a window between monitors, and when plugging in or unplugging an external non-retina monitor (if it causes the window to move from a retina to non-retina screen or vice-versa).
window.matchMedia is supported in IE10+, and all other modern browsers.
References: https://code.google.com/p/chromium/issues/detail?id=123694, MDN on min-resolution

Most (or all?) answers on the internet only detect a specific change. Typically they detect whether the value is 2 or something else.
The issue probably lies in the MediaQuery, because they only allow checking for specific hardcoded values.
With some programming, it's possible to dynamically create a media query, which checks for a change of the current value.
let remove = null;
const updatePixelRatio = () => {
if(remove != null) {
remove();
}
let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
let media = matchMedia(mqString);
media.addListener(updatePixelRatio);
remove = function() {media.removeListener(updatePixelRatio)};
console.log("devicePixelRatio: " + window.devicePixelRatio);
}
updatePixelRatio();

I took the IMO best answer (by #Neil) and made it a bit more human-readable:
function listenOnDevicePixelRatio() {
function onChange() {
console.log("devicePixelRatio changed: " + window.devicePixelRatio);
listenOnDevicePixelRatio();
}
matchMedia(
`(resolution: ${window.devicePixelRatio}dppx)`
).addEventListener("change", onChange, { once: true });
}
listenOnDevicePixelRatio();
No fixed boundary or variables needed.

Thanks #florian-kirmaier this is exactly what I was looking for and if you pass in the option {once: true} in the event listener there is no need to manually keep track and remove the event listener.
(function updatePixelRatio(){
matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
.addEventListener('change', updatePixelRatio, {once: true});
console.log("devicePixelRatio: " + window.devicePixelRatio);
})();

I prefer this one, so that I can provide a callback, and for the callback not to fire initially but only on changes, and to be able to stop it when no longer needed:
function onPixelRatioChange(cb) {
let mediaQuery
const listenerOptions = { once: true }
let firstRun = true
function onChange() {
if (firstRun) firstRun = false
else cb()
mediaQuery = matchMedia(`(resolution: ${devicePixelRatio}dppx)`)
mediaQuery.addEventListener('change', onChange, listenerOptions)
}
onChange()
return function unlisten() {
mediaQuery.removeEventListener('change', onChange, listenerOptions)
}
}
// Then use it like this:
const unlisten = onPixelRatioChange(() => {
console.log('pixel ratio changed:', devicePixelRatio)
})
// later, stop listening if desired:
unlisten()

Here's a typescript object version of #Florian's answer
export default class DevicePixelRatioObserver {
mediaQueryList: MediaQueryList | null = null
constructor(readonly onDevicePixelRatioChanged: () => void) {
this._onChange = this._onChange.bind(this)
this.createMediaQueryList()
}
createMediaQueryList() {
this.removeMediaQueryList()
let mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
this.mediaQueryList = matchMedia(mqString);
this.mediaQueryList.addEventListener('change', this._onChange)
}
removeMediaQueryList() {
this.mediaQueryList?.removeEventListener('change', this._onChange)
this.mediaQueryList = null
}
_onChange(event: MediaQueryListEvent) {
this.onDevicePixelRatioChanged()
this.createMediaQueryList()
}
destroy() {
this.removeMediaQueryList()
}
}

Related

Knowing when a custom Event is Added or Removed?

I have a Javascript class, Engine, that will create new events (eg const event = new Event('build');) and then dispatch them at the appropriate time, when Intervals elapse or it catches certain events, using elem.dispatchEvent(event);
I have other classes eg MyButton that will listen for the events eg elem.addEventListener('build', myListener, false);. NOTE: MyButton is not just a button - it's a class that adds a button, listens to it and other events, and does various tasks both in response to a click and through other ways.
Main question: is there a way for the Engine class to intercept, catch or otherwise know when its event is added and removed, so that it can start and stop the Intervals etc. as needed? Basically an onAddEventListener event, or a way to achieve the same effect. I can see that it could eg have functions for adding the event listeners:
AddIntervalListener(listener) {
// check if the Interval is started, and if not then start it
if (this.waitingIntervalId == null)
this.waitingIntervalId = setInterval(catchIntervalAndFireEvent, 500);
// add the event for the caller
elem.addEventListener('build', listener, false);
}
But is there a better way so that MyButton can call addEventListener() directly, and Engine knows and can start the Interval as needed?
Minor question: Engine is not associated with a particular element in the page. Is there a best practice choice for which element in the page Engine should use to fire the events, ie the elem in elem.addEventListener()? eg window or document or something else? Why do custom events need to be dispatched to an object? says I have to but it doesn't address which to use. Or should I take its implied advice, and make my own event/listener system?
Since you are already using your own button implementations, simply implement a addEventListener() method there which raises a customEventAdded event whenever it is called:
Here's an example:
class Engine {
constructor() {
this.handleCustomEventAdded = this.handleCustomEventAdded.bind(this); // do this if you need to access the engine instance inside the listener
document.addEventListener('customEventAdded', this.handleCustomEventAdded);
}
handleCustomEventAdded(event) {
console.log(`Engine learned: a ${event.detail.element.tagName} element was added a listener for ${event.detail.name}`)
}
}
class MyButton extends HTMLButtonElement {
addEventListener(event, handler, capture) {
// pass the method call to HTMLButtonElement.prototype.addEventListener
super.addEventListener(event, handler, capture);
console.log(`Eventlistener for ${event} added`);
switch (event) {
case 'build':
let evt = new CustomEvent(
'customEventAdded', {
bubbles: true,
detail: {
name: event,
element: this
}
}
);
this.dispatchEvent(evt);
break;
default: // noop
}
}
}
customElements.define('my-button', MyButton, {
extends: 'button'
});
new Engine();
document.getElementById('foo').addEventListener('build', function(event) {
// whatever
})
<button is="my-button" id="foo">My derived button</button>
This is a very simple, inelegant solution compared to #connexo's. Still, it does what I needed. Each method has different benefits and disadvantages. I had hoped for something in between, for example where Engine's Subscriber management instead uses events, or perhaps something else; if you have such a solution, please do put it here.
class Engine {
intervalTime = 200;
intervalId = null;
subscribers = [];
constructor(intervalTime) {
if (intervalTime!==undefined)
{ // parameter not omitted in call
this.intervalTime = intervalTime;
}
this.initialise();
}
initialise() {
window.addEventListener("load", this.windowLoadEvent.bind(this));
this.intervalId = setInterval(this.doInterval.bind(this), this.intervalTime);
}
// =======================
addSubscriber(subscriber) {
// note: does not check if subscriber already subscribed
// could check here if this is the first subscriber and only then start Interval/listening to event
this.subscribers.push(subscriber);
}
removeSubscriber(subscriber) {
// could check here if this is the last subscriber and stop any Interval/listening to event
this.subscribers = this.subscribers.filter(val => val !== subscriber);
}
// =======================
windowLoadEvent() {
// do stuff
this.doEvent();
}
// =======================
doInterval() {
for (let i=0; i<this.subscribers.length; i++)
this.subscribers[i].doRegularInterval();
}
doEvent(myVariable) {
for (let i=0; i<this.subscribers.length; i++)
this.subscribers[i].doEvent(myVariable);
}
}
class ButtonManager {
myEngine = null;
constructor(myEngine) {
this.myEngine = myEngine;
this.initialise();
}
initialise() {
myEngine.addSubscriber(this);
}
// =======================
doInterval() {
// do stuff
}
doURLChanged(myVariable) {
// do stuff
}
}
let theEngine = new Engine();
let theButton = new ButtonManager(theEngine);

How can I convert scrollIntoView with smooth animation to a Promise?

I have to scrollIntoView a particular element smoothly and then do something.
Example
element.scrollIntoView({behavior: 'smooth'}).then(() => {
// Do something here
})
I know that it can't be done this way as native scrollIntoView doesn't return a Promise. But, how do I achieve something like this?
I'm using Angular 7 BTW. So if there are any directives that could help me achieve this, it would be great.
You can work with prototypes, I think this could be a solution to your problem without download any npm packages
/* Extends Element Objects with a function named scrollIntoViewPromise
* options: the normal scrollIntoView options without any changes
*/
Element.prototype.scrollIntoViewPromise = function(options){
// "this" refers to the current element (el.scrollIntoViewPromise(options): this = el)
this.scrollIntoView(options);
// I create a variable that can be read inside the returned object ({ then: f() }) to expose the current element
let parent = this;
// I return an object with just a property inside called then
// then contains a function which accept a function as parameter that will be execute when the scroll ends
return {
then: function(x){
// Check out https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API for more informations
const intersectionObserver = new IntersectionObserver((entries) => {
let [entry] = entries;
// When the scroll ends (when our element is inside the screen)
if (entry.isIntersecting) {
// Execute the function into then parameter and stop observing the html element
setTimeout(() => {x(); intersectionObserver.unobserve(parent)}, 100)
}
});
// I start to observe the element where I scrolled
intersectionObserver.observe(parent);
}
};
}
element.scrollIntoViewPromise({behavior: "smooth"}).then(()=>console.log("EHI!"));
I've created an example. I know it's not an angular application, but it's a good starting point. You just need to implement it (If you're using typescript you have to create an interface which extends Element with the new function).
One way you can solve this is by using smooth-scroll-into-view-if-nedded it actually return a promise so you can bind to it and apply your logic.
There is an idea how you may catch animation ending.
You may do it in vanilla JS with a 'scroll' event listener.
Check this example https://codepen.io/Floky87/pen/BEOYvN
var hiddenElement = document.getElementById("box");
var btn = document.querySelector(".btn");
var isScrolling;
function handleScroll(event) {
// Clear our timeout throughout the scroll
window.clearTimeout(isScrolling);
// Set a timeout to run after scrolling ends
isScrolling = setTimeout(function() {
alert(1);
document.removeEventListener("scroll", handleScroll);
}, 66);
}
function handleButtonClick() {
document.addEventListener("scroll", handleScroll, false);
hiddenElement.scrollIntoView({ block: "center", behavior: "smooth" });
}
btn.addEventListener("click", handleButtonClick);
I made it like this
const scrollIntoViewPromise = async (node: HTMLElement, options?: ScrollIntoViewOptions) => {
node.scrollIntoView(options);
return new Promise((resolve) => {
const intersectionObserver = new IntersectionObserver((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setTimeout(() => {
resolve(true);
intersectionObserver.unobserve(node);
}, 100);
}
});
intersectionObserver.observe(node);
});
};

Is there any maps per page limitation in mapbox-gl?

I'm trying to have 17 small maps on the same page using mapbox-gl and facing:
WARNING: Too many active WebGL contexts. Oldest context will be lost.
Uncaught TypeError: Failed to execute 'shaderSource' on 'WebGLRenderingContext': parameter 1 is not of type 'WebGLShader'.
at new Program (mapbox-gl.js:182)
at Painter._createProgramCached (mapbox-gl.js:178)
at Painter.useProgram (mapbox-gl.js:178)
at setFillProgram (mapbox-gl.js:154)
at drawFillTile (mapbox-gl.js:154)
at drawFillTiles (mapbox-gl.js:154)
at Object.drawFill [as fill] (mapbox-gl.js:154)
at Painter.renderLayer (mapbox-gl.js:178)
at Painter.render (mapbox-gl.js:178)
at e._render (mapbox-gl.js:497)
I had the same issue when i tried to have many google streetview galleries on the same page, but as my streetview shouldn't be visible at the same moment i ended using the same streetview changing address dynamically.
But for maps list requirement is to show that many maps to user. Can't show them one by one. Not sure how i could work out that issue.
i'm using mapbox-gl#0.45.0, and testing it in chrome Version 66.0.3359.181 (Official Build) (64-bit) on Mac OS Sierra 10.12.6 (16G1036)
I'm going to guess you are out of luck. Browsers limit the number of WebGL instances. There are workarounds but to use them would probably require changes to the way mapbox-gl is implemented. I suggest you ask them if they'd consider implementing one of the workarounds assuming they haven't already.
There is one other possibility that comes to mind and that would be to do your own virtualization of WebGL in JavaScript. That's probably not a good solution though because it wouldn't share resources across maps and it might be too heavy.
Off the top of my head you'd have to create an offscreen canvas and override HTMLCanvasElement.prototype.getContext so that when someone makes a webgl context you return a virtual context. You'd wrap every function and if that virtual context doesn't match the last used virtual context you'd save all the webgl state and restore the state for the new context. You'd also have to keep framebuffers to match the drawingbuffer for each canvas, bind them when the current framebuffer binding is null and resize them if the canvas sized changed, and then render to the offscreen canvas and then canvas2d.drawImage to their respective canvases anytime the current event exits. It's that last part that would be heaviest.
In semi-pseudo code
// This is just off the top of my head and is just pseudo code
// but hopefully gives an idea of how to virtualize WebGL.
const canvasToVirtualContextMap = new Map();
let currentVirtualContext = null;
let sharedWebGLContext;
const baseState = makeDefaultState();
HTMLCanvasElement.prototype.getContext = (function(origFn) {
return function(type, contextAttributes) {
if (type === 'webgl') {
return createOrGetVirtualWebGLContext(this, type, contextAttributes);
}
return origFn.call(this, contextAttributes);
};
}(HTMLCanvasElement.prototype.getContext));
class VirutalWebGLContext {
constructor(cavnas, contextAttributes) {
this.canvas = canvas;
// based on context attributes and canvas.width, canvas.height
// create a texture and framebuffer
this._drawingbufferTexture = ...;
this._drawingbufferFramebuffer = ...;
// remember all WebGL state (default bindings, default texture units,
// default attributes and/or vertex shade object, default program,
// default blend, stencil, zbuffer, culling, viewport etc... state
this._state = makeDefaultState();
}
}
function makeDefaultState() {
const state ={};
state[WebGLRenderingContext.ARRAY_BUFFER] = null;
... tons more ...
}
// copy all WebGL constants and functions to the prototype of
// VirtualWebGLContext
for (let key in WebGLRenderingContext.protoype) {
const value = WebGLRenderingContext.prototype[key];
let newValue = value;
switch (key) {
case 'bindFramebuffer':
newValue = virutalBindFramebuffer;
break;
case 'clear':
case 'drawArrays':
case 'drawElements':
newValue = createDrawWrapper(value);
break;
default:
if (typeof value === 'function') {
newValue = createWrapper(value);
}
break;
}
VirtualWebGLContext.prototype[key] = newValue;
}
function virutalBindFramebuffer(bindpoint, framebuffer) {
if (bindpoint === WebGLRenderingContext.FRAMEBUFFER) {
if (target === null) {
// bind our drawingBuffer
sharedWebGLContext.bindFramebuffer(bindpoint, this._drawingbufferFramebuffer);
}
}
sharedWebGLContext.bindFramebuffer(bindpoint, framebuffer);
}
function createWrapper(origFn) {
// lots of optimization could happen here depending on specific functions
return function(...args) {
makeCurrentContext(this);
resizeCanvasIfChanged(this);
return origFn.call(sharedWebGLContext, ...args);
};
}
function createDrawWrapper(origFn) {
const newFn = createWrapper(origFn);
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
this._needComposite = true;
return newFn.call(this, ...args);
};
}
function makeCurrentContext(vctx) {
if (currentVirtualContext === vctx) {
return;
}
// save all current WebGL state on the previous current virtual context
saveAllState(currentVirutalContext._state);
// restore all state for the
restoreAllState(vctx._state);
// check if the current state is supposed to be rendering to the canvas.
// if so bind vctx._drawingbuffer
currentVirtualContext = vctx;
}
function resizeCanvasIfChanged(vctx) {
if (canvas.width !== vtx._width || canvas.height !== vctx._height) {
// resize this._drawingBuffer to match the new canvas size
}
}
function createOrGetVirtualWebGLContext(canvas, type, contextAttributes) {
// check if this canvas already has a context
const existingVirtualCtx = canvasToVirtualContextMap.get(canvas);
if (existingVirtualCtx) {
return existingVirtualCtx;
}
if (!sharedWebGLContext) {
sharedWebGLContext = document.createElement("canvas").getContext("webgl");
}
const newVirtualCtx = new VirtualWebGLContext(canvas, contextAttributes);
canvasToVirtualContextMap.set(canvas, newVirtualCtx);
return newVirtualCtx;
}
function saveAllState(state) {
// save all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
state[WebGLRenderingContext.ARRAY_BUFFER] = sharedGLState.getParameter(gl.ARRAY_BUFFER_BINDING);
state[WebGLRenderingContext.TEXTURE_2D] = sharedGLState.getParameter(gl.TEXTURE_BINDING_2D);
... tons more ...
}
function restoreAllState(state) {
// resture all WebGL state (current bindings, current texture units,
// current attributes and/or vertex shade object, current program,
// current blend, stencil, zbuffer, culling, viewport etc... state
gl.bindArray(gl.ARRAY_BUFFER, state[WebGLRenderingContext.ARRAY_BUFFER]);
gl.bindTexture(gl.TEXTURE_2D, state[WebGLRenderingContext.TEXTURE_2D]);
... tons more ...
}
function renderAllDirtyVirtualCanvas() {
let setup = false;
for (const vctx of canvasToVirtualContextMap.values()) {
if (!vctx._needComposite) {
continue;
}
vctx._needComposite = false;
if (!setup) {
setup = true;
// save all current WebGL state on the previous current virtual context
saveAllState(currentVirutalContext._state);
currentVirutalContext = null;
// set the state back to the default
restoreAllState(sharedGlContext, baseState);
// setup whatever state we need to render vctx._drawinbufferTexture
// to the canvas.
sharedWebGLContext.useProgram(programToRenderCanvas);
...
}
// draw the drawingbuffer's texture to the canvas
sharedWebGLContext.bindTexture(gl.TEXTURE_2D, vctx._drawingbufferTexture);
sharedWebGLContext.drawArrays(gl.TRIANGLES, 0, 6);
}
}
you'd also need to trap events that cause rendering which would be unique to each app. If the app uses requetsAnimationFrame to render then maybe something like
window.requestAnimationFrame = (function(origFn) {
return function(callback) {
return origFn.call(window, (time) {
const result = callback(time);
renderAllDirtyVirtualCanvases();
return result;
};
};
}(window.requestAnimationFrame));
If the app renders on other events, like say mousemove then maybe
something like this
let someContextNeedsRendering;
function createDrawWrapper(origFn) {
const newFn = createWrapper(origFn);
return function(...args) {
// a rendering function was called so we need to copy are drawingBuffer
// to the canvas for this context after the current event.
this._needComposite = true;
if (!someContextsNeedRendering) {
someContextsNeedRendering = true;
setTimeout(dealWithDirtyContexts, 0);
}
return newFn.call(this, ...args);
};
}
function dealWithDirtyContexts() {
someContextsNeedRendering = false;
renderAllDirtyVirtualCanvas();
});
Makes me wonder if someone else has already done this.

Function with memoization, how not to evaluate each time?

I'm writing a simple jQuery function that will swap some HTML elements for others when on certain viewports. The idea is simple:
<div data-swap-for="#element" data-swap-on="phone"></div>
Will insert the element with id #element after that line when the current media query corresponds to phone (the details about how that is done are not important).
My function looks like this:
jq.fn.swapElements = function(viewport) {
var targets = jq('[data-swap-for][data-swap-on='+viewport+']');
if (targets.length) {
console.log('Found elements to swap for', viewport);
} else {
console.log('Found no elements to swap for', viewport);
}
return {
on: function() {
console.log('Should swap elements for', viewport);
},
off: function() {
console.log('Should restore elements', viewport);
}
}
};
So whenever the screen enters the phone layout, it calls:
jq().swapElements('phone').on();
Which should do all the DOM transformations, and when it exits the phone layout, it calls:
jq().swapElements('phone').off();
Which should restore them.
My problem is that these two are creating a new evaluation of the var targets... part, resulting in:
As the output in the console, and I need this function to cache or remember the variables that it uses, so that the resulting console output is:
> Found elements to swap for phone
> Should swap elements for phone
That is, only evaluating the elements and saving the variables once per each call (a different viewport value should call for a new evaluation).
I've been looking into higher order functions and memoization, but I'm confused about how to apply this in this case and specially to a jQuery function.
Please help?
Thanks
You can use some variable (object or array) to cache already targeted elements.
var cache = {}; // Should be out of function
if (viewport in cache) {
var targets = cache[viewport];
} else {
var targets = jq('[data-swap-for][data-swap-on='+viewport+']');
cache[viewport] = targets;
}
I Would go with slightly different approach:
jq.fn.swapElements = {
var cache;
getTargets: function(viewport) {
if (viewport in this.cache) {
return cache[viewport];
} else {
var targets = jq('[data-swap-for][data-swap-on='+viewport+']');
if (targets.length) {
console.log('Found elements to swap for', viewport);
} else {
console.log('Found no elements to swap for', viewport);
}
this.cache[viewport] = targets;
return this.cache[viewport];
}
}
on: function(viewport) {
console.log('Should swap elements for', viewport);
},
off: function(viewport) {
console.log('Should restore elements', viewport);
}
};
Pseudocode might not work in particular case, but You get the idea. Whenever You need targets you call swapElements.getTargets(viewport) function.
I'm pretty sure you don't need a higher-order memoize function (although you could trivially apply it when you have written one anyway).
What you need to do is to store the result of jq().swapElements('phone') in a variable, and when the screen enters/exits the phone layout you should call the methods on that variable, instead of creating new instances.

JavaScript window resize event

How can I hook into a browser window resize event?
There's a jQuery way of listening for resize events but I would prefer not to bring it into my project for just this one requirement.
Best practice is to add to the resize event, rather than replace it:
window.addEventListener('resize', function(event) {
...
}, true);
An alternative is to make a single handler for the DOM event (but can only have one), eg.
window.onresize = function(event) {
...
};
jQuery may do some work to ensure that the resize event gets fired consistently in all browsers, but I'm not sure if any of the browsers differ, but I'd encourage you to test in Firefox, Safari, and IE.
First off, I know the addEventListener method has been mentioned in the comments above, but I didn't see any code. Since it's the preferred approach, here it is:
window.addEventListener('resize', function(event){
// do stuff here
});
Here's a working sample.
Never override the window.onresize function.
Instead, create a function to add an Event Listener to the object or element.
This checks and incase the listeners don't work, then it overrides the object's function as a last resort. This is the preferred method used in libraries such as jQuery.
object: the element or window object
type: resize, scroll (event type)
callback: the function reference
var addEvent = function(object, type, callback) {
if (object == null || typeof(object) == 'undefined') return;
if (object.addEventListener) {
object.addEventListener(type, callback, false);
} else if (object.attachEvent) {
object.attachEvent("on" + type, callback);
} else {
object["on"+type] = callback;
}
};
Then use is like this:
addEvent(window, "resize", function_reference);
or with an anonymous function:
addEvent(window, "resize", function(event) {
console.log('resized');
});
Solution for 2018+:
You should use ResizeObserver. It is a browser-native solution that has a much better performance than to use the resize event. In addition, it not only supports the event on the document but also on arbitrary elements.
var ro = new ResizeObserver( entries => {
for (let entry of entries) {
const cr = entry.contentRect;
console.log('Element:', entry.target);
console.log(`Element size: ${cr.width}px x ${cr.height}px`);
console.log(`Element padding: ${cr.top}px ; ${cr.left}px`);
}
});
// Observe one or multiple elements
ro.observe(someElement);
Currently, Firefox, Chrome, Safari, and Edge support it. For other (and older) browsers you have to use a polyfill.
The resize event should never be used directly as it is fired continuously as we resize.
Use a debounce function to mitigate the excess calls.
window.addEventListener('resize',debounce(handler, delay, immediate),false);
Here's a common debounce floating around the net, though do look for more advanced ones as featuerd in lodash.
const debounce = (func, wait, immediate) => {
var timeout;
return () => {
const context = this, args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
};
This can be used like so...
window.addEventListener('resize', debounce(() => console.log('hello'),
200, false), false);
It will never fire more than once every 200ms.
For mobile orientation changes use:
window.addEventListener('orientationchange', () => console.log('hello'), false);
Here's a small library I put together to take care of this neatly.
I do believe that the correct answer has already been provided by #Alex V, yet the answer does require some modernization as it is over five years old now.
There are two main issues:
Never use object as a parameter name. It is a reservered word. With this being said, #Alex V's provided function will not work in strict mode.
The addEvent function provided by #Alex V does not return the event object if the addEventListener method is used. Another parameter should be added to the addEvent function to allow for this.
NOTE: The new parameter to addEvent has been made optional so that migrating to this new function version will not break any previous calls to this function. All legacy uses will be supported.
Here is the updated addEvent function with these changes:
/*
function: addEvent
#param: obj (Object)(Required)
- The object which you wish
to attach your event to.
#param: type (String)(Required)
- The type of event you
wish to establish.
#param: callback (Function)(Required)
- The method you wish
to be called by your
event listener.
#param: eventReturn (Boolean)(Optional)
- Whether you want the
event object returned
to your callback method.
*/
var addEvent = function(obj, type, callback, eventReturn)
{
if(obj == null || typeof obj === 'undefined')
return;
if(obj.addEventListener)
obj.addEventListener(type, callback, eventReturn ? true : false);
else if(obj.attachEvent)
obj.attachEvent("on" + type, callback);
else
obj["on" + type] = callback;
};
An example call to the new addEvent function:
var watch = function(evt)
{
/*
Older browser versions may return evt.srcElement
Newer browser versions should return evt.currentTarget
*/
var dimensions = {
height: (evt.srcElement || evt.currentTarget).innerHeight,
width: (evt.srcElement || evt.currentTarget).innerWidth
};
};
addEvent(window, 'resize', watch, true);
window.onresize = function() {
// your code
};
The following blog post may be useful to you: Fixing the window resize event in IE
It provides this code:
Sys.Application.add_load(function(sender, args) {
$addHandler(window, 'resize', window_resize);
});
var resizeTimeoutId;
function window_resize(e) {
window.clearTimeout(resizeTimeoutId);
resizeTimeoutId = window.setTimeout('doResizeCode();', 10);
}
The already mentioned solutions above will work if all you want to do is resize the window and window only. However, if you want to have the resize propagated to child elements, you will need to propagate the event yourself. Here's some example code to do it:
window.addEventListener("resize", function () {
var recResizeElement = function (root) {
Array.prototype.forEach.call(root.childNodes, function (el) {
var resizeEvent = document.createEvent("HTMLEvents");
resizeEvent.initEvent("resize", false, true);
var propagate = el.dispatchEvent(resizeEvent);
if (propagate)
recResizeElement(el);
});
};
recResizeElement(document.body);
});
Note that a child element can call
event.preventDefault();
on the event object that is passed in as the first Arg of the resize event. For example:
var child1 = document.getElementById("child1");
child1.addEventListener("resize", function (event) {
...
event.preventDefault();
});
You can use following approach which is ok for small projects
<body onresize="yourHandler(event)">
function yourHandler(e) {
console.log('Resized:', e.target.innerWidth)
}
<body onresize="yourHandler(event)">
Content... (resize browser to see)
</body>
<script language="javascript">
window.onresize = function() {
document.getElementById('ctl00_ContentPlaceHolder1_Accordion1').style.height = '100%';
}
</script>
var EM = new events_managment();
EM.addEvent(window, 'resize', function(win,doc, event_){
console.log('resized');
//EM.removeEvent(win,doc, event_);
});
function events_managment(){
this.events = {};
this.addEvent = function(node, event_, func){
if(node.addEventListener){
if(event_ in this.events){
node.addEventListener(event_, function(){
func(node, event_);
this.events[event_](win_doc, event_);
}, true);
}else{
node.addEventListener(event_, function(){
func(node, event_);
}, true);
}
this.events[event_] = func;
}else if(node.attachEvent){
var ie_event = 'on' + event_;
if(ie_event in this.events){
node.attachEvent(ie_event, function(){
func(node, ie_event);
this.events[ie_event]();
});
}else{
node.attachEvent(ie_event, function(){
func(node, ie_event);
});
}
this.events[ie_event] = func;
}
}
this.removeEvent = function(node, event_){
if(node.removeEventListener){
node.removeEventListener(event_, this.events[event_], true);
this.events[event_] = null;
delete this.events[event_];
}else if(node.detachEvent){
node.detachEvent(event_, this.events[event_]);
this.events[event_] = null;
delete this.events[event_];
}
}
}

Categories

Resources