JavaScript event for canvas resize - javascript

It appears that altering the dimensions of a canvas clears any drawing already done on that canvas.
Is there an event that fires on canvas resize, so I can hook it and redraw when it occurs?

Update 2020
Jemenake's answer looks good, but in general I would recommend loading in a library which provides debounce functionality, as that's what this is called.
For example with the lodash one, you can do window.addEventListner('resize', _.debounce(onResize, 500). Note that there is a third argument where you can specify the behavior you want (e.g. window.addEventListner('resize', _.debounce(onResize, 500, {leading: true, trailing true})), although the default should be pretty good.
Kit Sunde's answer will do a lot of unnecessary work whilst the browser window is not resized. It is better to check whether the event got resized in response to a browser event and next ignore resize events for a given amount of time after which you do a check again (this will cause two checks after eachother in quick succession and the code could be improved to prevent this, but you get the idea probably).
(function(){
var doCheck = true;
var check = function(){
//do the check here and call some external event function or something.
};
window.addEventListener("resize",function(){
if(doCheck){
check();
doCheck = false;
setTimeout(function(){
doCheck = true;
check();
},500)
}
});
})();
Please note, the code above was typed blindly and not checked.

David Mulder's answer is an improvement, but it looks like it will trigger after waiting timeout milliseconds after the first resize event. In other words, if more resizes happen before the timeout, it doesn't reset the timer. I was looking for the opposite; I wanted something which would wait a little bit of time to let the resize events stop, and then fire after a certain amount of time after the last one. The following code does that.
The ID of any currently-running timer will be in timer_id. So, whenever there's a resize, it checks to see if there's already a time running. If so, it cancels that one and starts a new one.
function setResizeHandler(callback, timeout) {
var timer_id = undefined;
window.addEventListener("resize", function() {
if(timer_id != undefined) {
clearTimeout(timer_id);
timer_id = undefined;
}
timer_id = setTimeout(function() {
timer_id = undefined;
callback();
}, timeout);
});
}
function callback() {
// Here's where you fire-after-resize code goes.
alert("Got called!");
}
setResizeHandler(callback, 1500);

You usually don't want to strictly check for a resize event because they fire a lot when you do a dynamic resize, like $(window).resize in jQuery and as far I'm aware there is no native resize event on elements (there is on window). I would check it on an interval instead:
function onResize( element, callback ){
var elementHeight = element.height,
elementWidth = element.width;
setInterval(function(){
if( element.height !== elementHeight || element.width !== elementWidth ){
elementHeight = element.height;
elementWidth = element.width;
callback();
}
}, 300);
}
var element = document.getElementsByTagName("canvas")[0];
onResize( element, function(){ alert("Woo!"); } );

I didn't find any build-in events to detect new canvas dimensions, so I tried to find a workaround for two scenarios.
function onresize()
{
// handle canvas resize event here
}
var _savedWidth = canvas.clientWidth;
var _savedHeight = canvas.clientHeight;
function isResized()
{
if(_savedWidth != canvas.clientWidth || _savedHeight != canvas.clientHeight)
{
_savedWidth = canvas.clientWidth;
_savedHeight = canvas.clientHeight;
onresize();
}
}
window.addEventListener("resize", isResized);
(new MutationObserver(function(mutations)
{
if(mutations.length > 0) { isResized(); }
})).observe(canvas, { attributes : true, attributeFilter : ['style'] });
For detecting any JavaScript canvas.style changes, the MutationObserver API is good for. It doesn't detect a specific style property, but
in this case it is sufficient to check if canvas.clientWidth and canvas.clientHeight has changed.
If the canvas size is declared in a style-tag with a percent unit, we have a problem to interpret the css selectors correctly (>, *, ::before, ...) by using JavaScript. I think in this case the best way is to set a window.resize handler and also check the canvas client size.

Have you tried ResizeObserver (caniuse.com) in addition to the debounce logic mentioned in the other answers like Jemenake's?
For example:
const pleasantText = 'There is a handle! Let\'s try relaxing while resizing the awesome canvas! ->';
onResize = (entries) => {
document.getElementsByTagName('span')[0].innerHTML =
`${pleasantText} <b>${entries[0].target.offsetWidth}x${entries[0].target.offsetHeight}</b>`;
};
const canvas = document.getElementsByTagName('canvas')[0];
const observer = new ResizeObserver(onResize);
observer.observe(canvas);
body > div:first-of-type {
display: grid;
place-items: center;
width: 100vw;
height: 100vh;
user-select: none;
}
body > div:first-of-type div {
display: grid;
min-width: 12em;
min-height: 6em;
width: 16rem;
height: 6rem;
background: #ccc;
overflow: auto;
place-items: center;
text-align: center;
resize: both;
grid-auto-columns: 20vw 1fr;
grid-auto-flow: column;
grid-gap: 20px;
padding: 0.4rem 0.8rem;
border-radius: 4px;
}
canvas {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
background: #ff9;
}
<div>
<div>
<span></span>
<canvas></canvas>
</div>
</div>

save the canvas state as imageData and then redraw it after resizing
var imgDat=ctx.getImageData(0,0,ctx.canvas.width,ctx.canvas.height)
after resize
ctx.putImageData(imgDat,0,0)

Related

Always listen to window screen changes

How do I make the code always respond to the changes in screen size?
I remember there is one globalEventHandler can do this but I'm not sure which one...
For example, the div#test-02 will always automatically adjust its width to 1/3 of the div#test-01. The problem is that as we use the developer tool (f12) to resize the window, the width of div#test-01 is keep changing but the code won't respond to it anymore...Unless we reload the page...
const test01 = document.querySelector('#test-01');
const test02 = document.querySelector('#test-02');
const data = test01.getBoundingClientRect().width;
test02.style.width = `${data / 3}px`;
#test-01 {
width: 100vw;
height: 50vh;
background-color: grey;
display: flex;
justify-content: center;
align-items: center;
}
#test-02 {
height: 100%;
background-color: orange;
}
<div id="test-01">
<div id="test-02"></div>
</div>
To listen for window screen changes, you can use
window.addEventListener("resize", callback)
This will listen for resize events on the window, and execute the callback function if such an event occurs. So you could put the code which resizes your elements relative to each other into the callback, so that it is executed on every window resize. (The callback will not be run on page load, so you would have to run your code outside of the event handler once too.)
The following code should work in your case:
const test01 = document.querySelector('#test-01');
const test02 = document.querySelector('#test-02');
function onResize() {
const data = test01.getBoundingClientRect().width;
test02.style.width = `${data / 3}px`;
}
window.addEventListener("resize", onResize, {passive: true})
onResize()

Is it possible to add a class to an element when positioned sticky to the top of the window? [duplicate]

I'm using the new position: sticky (info) to create an iOS-like list of content.
It's working well and far superior than the previous JavaScript alternative (example) however as far as I know no event is fired when it's triggered, which means I can't do anything when the bar hits the top of the page, unlike with the previous solution.
I'd like to add a class (e.g. stuck) when an element with position: sticky hits the top of the page. Is there a way to listen for this with JavaScript? Usage of jQuery is fine.
Demo with IntersectionObserver (use a trick):
// get the sticky element
const stickyElm = document.querySelector('header')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
header{
position: sticky;
top: -1px; /* ➜ the trick */
padding: 1em;
padding-top: calc(1em + 1px); /* ➜ compensate for the trick */
background: salmon;
transition: .1s;
}
/* styles for when the header is in sticky mode */
header.isSticky{
font-size: .8em;
opacity: .5;
}
<section>Space</section>
<header>Sticky Header</header>
The top value needs to be -1px or the element will never intersect with the top of the browser window (thus never triggering the intersection observer).
To counter this 1px of hidden content, an additional 1px of space should be added to either the border or the padding of the sticky element.
💡 Alternatively, if you wish to keep the CSS as is (top:0), then you can apply the "correction" at the intersection observer-level by adding the setting rootMargin: '-1px 0px 0px 0px' (as #mattrick showed in his answer)
Demo with old-fashioned scroll event listener:
auto-detecting first scrollable parent
Throttling the scroll event
Functional composition for concerns-separation
Event callback caching: scrollCallback (to be able to unbind if needed)
// get the sticky element
const stickyElm = document.querySelector('header');
// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);
// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;
// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)
// Act if sticky or not
const onSticky = isSticky => {
console.clear()
console.log(isSticky)
stickyElm.classList.toggle('isSticky', isSticky)
}
// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)
const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)
// OPTIONAL CODE BELOW ///////////////////
// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
var style = getComputedStyle(element),
excludeStaticParent = style.position === "absolute",
overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position !== "fixed")
for (var parent = element; (parent = parent.parentElement); ){
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static")
continue;
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
return parent;
}
return window
}
// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}
header{
position: sticky;
top: 0;
/* not important styles */
background: salmon;
padding: 1em;
transition: .1s;
}
header.isSticky{
/* styles for when the header is in sticky mode */
font-size: .8em;
opacity: .5;
}
/* not important styles*/
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
<section>Space</section>
<header>Sticky Header</header>
Here's a React component demo which uses the first technique
I found a solution somewhat similar to #vsync's answer, but it doesn't require the "hack" that you need to add to your stylesheets. You can simply change the boundaries of the IntersectionObserver to avoid needing to move the element itself outside of the viewport:
const observer = new IntersectionObserver(callback, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});
observer.observe(element);
If anyone gets here via Google one of their own engineers has a solution using IntersectionObserver, custom events, and sentinels:
https://developers.google.com/web/updates/2017/09/sticky-headers
Just use vanilla JS for it. You can use throttle function from lodash to prevent some performance issues as well.
const element = document.getElementById("element-id");
document.addEventListener(
"scroll",
_.throttle(e => {
element.classList.toggle(
"is-sticky",
element.offsetTop <= window.scrollY
);
}, 500)
);
After Chrome added position: sticky, it was found to be not ready enough and relegated to to --enable-experimental-webkit-features flag. Paul Irish said in February "feature is in a weird limbo state atm".
I was using the polyfill until it become too much of a headache. It works nicely when it does, but there are corner cases, like CORS problems, and it slows page loads by doing XHR requests for all your CSS links and reparsing them for the "position: sticky" declaration that the browser ignored.
Now I'm using ScrollToFixed, which I like better than StickyJS because it doesn't mess up my layout with a wrapper.
There is currently no native solution. See Targeting position:sticky elements that are currently in a 'stuck' state. However I have a CoffeeScript solution that works with both native position: sticky and with polyfills that implement the sticky behavior.
Add 'sticky' class to elements you want to be sticky:
.sticky {
position: -webkit-sticky;
position: -moz-sticky;
position: -ms-sticky;
position: -o-sticky;
position: sticky;
top: 0px;
z-index: 1;
}
CoffeeScript to monitor 'sticky' element positions and add the 'stuck' class when they are in the 'sticky' state:
$ -> new StickyMonitor
class StickyMonitor
SCROLL_ACTION_DELAY: 50
constructor: ->
$(window).scroll #scroll_handler if $('.sticky').length > 0
scroll_handler: =>
#scroll_timer ||= setTimeout(#scroll_handler_throttled, #SCROLL_ACTION_DELAY)
scroll_handler_throttled: =>
#scroll_timer = null
#toggle_stuck_state_for_sticky_elements()
toggle_stuck_state_for_sticky_elements: =>
$('.sticky').each ->
$(this).toggleClass('stuck', this.getBoundingClientRect().top - parseInt($(this).css('top')) <= 1)
NOTE: This code only works for vertical sticky position.
I came up with this solution that works like a charm and is pretty small. :)
No extra elements needed.
It does run on the window scroll event though which is a small downside.
apply_stickies()
window.addEventListener('scroll', function() {
apply_stickies()
})
function apply_stickies() {
var _$stickies = [].slice.call(document.querySelectorAll('.sticky'))
_$stickies.forEach(function(_$sticky) {
if (CSS.supports && CSS.supports('position', 'sticky')) {
apply_sticky_class(_$sticky)
}
})
}
function apply_sticky_class(_$sticky) {
var currentOffset = _$sticky.getBoundingClientRect().top
var stickyOffset = parseInt(getComputedStyle(_$sticky).top.replace('px', ''))
var isStuck = currentOffset <= stickyOffset
_$sticky.classList.toggle('js-is-sticky', isStuck)
}
Note: This solution doesn't take elements that have bottom stickiness into account. This only works for things like a sticky header. It can probably be adapted to take bottom stickiness into account though.
I know it has been some time since the question was asked, but I found a good solution to this. The plugin stickybits uses position: sticky where supported, and applies a class to the element when it is 'stuck'. I've used it recently with good results, and, at time of writing, it is active development (which is a plus for me) :)
I'm using this snippet in my theme to add .is-stuck class to .site-header when it is in a stuck position:
// noinspection JSUnusedLocalSymbols
(function (document, window, undefined) {
let windowScroll;
/**
*
* #param element {HTMLElement|Window|Document}
* #param event {string}
* #param listener {function}
* #returns {HTMLElement|Window|Document}
*/
function addListener(element, event, listener) {
if (element.addEventListener) {
element.addEventListener(event, listener);
} else {
// noinspection JSUnresolvedVariable
if (element.attachEvent) {
element.attachEvent('on' + event, listener);
} else {
console.log('Failed to attach event.');
}
}
return element;
}
/**
* Checks if the element is in a sticky position.
*
* #param element {HTMLElement}
* #returns {boolean}
*/
function isSticky(element) {
if ('sticky' !== getComputedStyle(element).position) {
return false;
}
return (1 >= (element.getBoundingClientRect().top - parseInt(getComputedStyle(element).top)));
}
/**
* Toggles is-stuck class if the element is in sticky position.
*
* #param element {HTMLElement}
* #returns {HTMLElement}
*/
function toggleSticky(element) {
if (isSticky(element)) {
element.classList.add('is-stuck');
} else {
element.classList.remove('is-stuck');
}
return element;
}
/**
* Toggles stuck state for sticky header.
*/
function toggleStickyHeader() {
toggleSticky(document.querySelector('.site-header'));
}
/**
* Listen to window scroll.
*/
addListener(window, 'scroll', function () {
clearTimeout(windowScroll);
windowScroll = setTimeout(toggleStickyHeader, 50);
});
/**
* Check if the header is not stuck already.
*/
toggleStickyHeader();
})(document, window);
#vsync 's excellent answer was almost what I needed, except I "uglify" my code via Grunt, and Grunt requires some older JavaScript code styles. Here is the adjusted script I used instead:
var stickyElm = document.getElementById('header');
var observer = new IntersectionObserver(function (_ref) {
var e = _ref[0];
return e.target.classList.toggle('isSticky', e.intersectionRatio < 1);
}, {
threshold: [1]
});
observer.observe( stickyElm );
The CSS from that answer is unchanged
Something like this also works for a fixed scroll height:
// select the header
const header = document.querySelector('header');
// add an event listener for scrolling
window.addEventListener('scroll', () => {
// add the 'stuck' class
if (window.scrollY >= 80) navbar.classList.add('stuck');
// remove the 'stuck' class
else navbar.classList.remove('stuck');
});

Manipulating the DOM using jQuery .resize()

I want to change the order of elements in the DOM based on different browser sizes.
I've looked into using intention.js but feel that it might be overkill for what I need (it depends on underscore.js).
So, i'm considering using jQuery's .resize(), but want to know if you think something like the following would be acceptable, and in line with best practices...
var layout = 'desktop';
$( window ).resize(function() {
var ww = $( window ).width();
if(ww<=767 && layout !== 'mobile'){
layout = 'mobile';
// Do something here
}else if((ww>767 && ww<=1023) && layout !== 'tablet'){
layout = 'tablet';
// Do something here
}else if(ww>1023 && layout !== 'desktop'){
layout = 'desktop';
// Do something here
}
}).trigger('resize');
I'm storing the current layout in the layout variable so as to only trigger the functions when the window enters the next breakpoint.
Media queries are generally preferred. However, if I am in a situation where I am in a single page application that has a lot of manipulation during runtime, I will use onresize() instead. Javascript gives you a bit more freedom to work with dynamically setting breakpoints (especially if you are moving elements around inside the DOM tree with stuff like append()). The setup you have is pretty close to the one I use:
function setWidthBreakpoints(windowWidth) {
if (windowWidth >= 1200) {
newWinWidth = 'lg';
} else if (windowWidth >= 992) {
newWinWidth = 'md';
} else if (windowWidth >= 768) {
newWinWidth = 'sm';
} else {
newWinWidth = 'xs';
}
}
window.onresize = function () {
setWidthBreakpoints($(this).width());
if (newWinWidth !== winWidth) {
onSizeChange();
winWidth = newWinWidth;
}
};
function onSizeChange() {
// do some size changing events here.
}
The one thing that you have not included that is considered best practice is a debouncing function, such as the one below provided by Paul Irish, which prevents repeated firing of the resize event in a browser window:
(function($,sr){
// debouncing function from John Hann
// http://unscriptable.com/index.php/2009/03/20/debouncing-javascript-methods/
var debounce = function (func, threshold, execAsap) {
var timeout;
return function debounced () {
var obj = this, args = arguments;
function delayed () {
if (!execAsap)
func.apply(obj, args);
timeout = null;
};
if (timeout)
clearTimeout(timeout);
else if (execAsap)
func.apply(obj, args);
timeout = setTimeout(delayed, threshold || 100);
};
}
// smartresize
jQuery.fn[sr] = function(fn){ return fn ? this.bind('resize', debounce(fn)) : this.trigger(sr); };
})(jQuery,'smartresize');
// usage:
$(window).smartresize(function(){
// code that takes it easy...
});
So incorporate a debouncer into your resize function and you should be golden.
In the practice is better to use Media Queries
Try this, I'm in a hurry atm and will refactor later.
SCSS:
body, html, .wrapper { width: 100%; height: 100% }
.sidebar { width: 20%; height: 500px; float: left;
&.mobile { display: none } }
.content { float: right; width: 80% }
.red { background-color: red }
.blue { background-color: blue }
.green { background-color: green }
#media all and (max-width: 700px) {
.content { width: 100%; float: left }
.sidebar { display: none
&.mobile { display: block; width: 100% }
}
}
HAML
.wrapper
.sidebar.blue
.content.red
.content.green
.sidebar.mobile.blue
On 700 px page breaks, sidebar disappears and mobile sidebar appears.
This can be much more elegant but you get the picture.
Only possible downside to this approach is duplication of sidebar.
That's it, no JS.
Ok, the reason for my original question was because I couldn't find a way to move a left sidebar (which appears first in the HTML) to appear after the content on mobiles.
Despite the comments, I still can't see how using media queries and position or display alone would reliably solve the problem (perhaps someone can give an example?).
But, it did lead me to investigate the flexbox model - display: flex, and so I have ended up using that, and specifically flex's order property to re-arrange the order of the sidebars and content area.
Good guide here - https://css-tricks.com/snippets/css/a-guide-to-flexbox/

Using JavaScript to change CSS style based on page width

I'm working on an assignment for a class where I have to make an external JavaScript file that checks a page's current width and changes the linked CSS style based on it, and have that occur whenever the page loads or is resized. Normally, we are given an example to base our assignment off of, but that was not the case this time around.
Essentially we are to use an if...then statement to change the style. I have no clue what the appropriate statements would be for the function. I've looked around and the potential solutions are either too advanced for the class or don't go over what I need. As far as I know I cannot use jQuery or CSS queries.
If someone could give me an example of how I would write this out, I would be very appreciative.
Try this code
html is
<div id="resize" style="background:red; height: 100px; width: 100px;"></div>
javascript is
var resize = document.getElementById('resize');
window.onresize=function(){
if(window.innerWidth <= 500) {
resize.style = "background:blue; height: 100px; width: 100px;";
}
else {
resize.style = "background:red; height: 100px; width: 100px;";
}
};
i have create a sample for you look at here TESTRESIZE
try resizing your browser and let me know if it is what you are looking for.
//Use This//
function adjustStyle() {
var width = 0;
// get the width.. more cross-browser issues
if (window.innerHeight) {
width = window.innerWidth;
} else if (document.documentElement && document.documentElement.clientHeight) {
width = document.documentElement.clientWidth;
} else if (document.body) {
width = document.body.clientWidth;
}
// now we should have it
if (width < 799) {
document.getElementById("CSS").setAttribute("href", "http://carrasquilla.faculty.asu.edu/GIT237/smallStyle.css");
} else {
document.getElementById("CSS").setAttribute("href", "http://carrasquilla.faculty.asu.edu/GIT237/largeStyle.css");
}
}
// now call it when the window is resized.
window.onresize = function () {
adjustStyle();
};
window.onload = function () {
adjustStyle();
};
Use CSS and media queries. It's bad idea to change style by js.

Natural window resize event invalidating layout in Chrome, forced window resize does not.

Demonstration: http://jsfiddle.net/calvintennant/NrJ8T/show/
When I force a window resize by doing: $(window).resize() my listener is called, and everything is fine. However if I actually resize the window, I'm getting multiple resize events called within the same frame.
Timeline during forced resize:
Timeline during natural resize:
Is this a bug in Chrome, or am I misunderstanding something?
As pointed out by #avram-lavinsky, resize events can be called multiple time per frame.
Updated example using Request Animation Frame (seen first here https://developer.mozilla.org/en-US/docs/Web/Reference/Events/resize):
http://jsfiddle.net/calvintennant/v69WW/
# HTML
<div class="box-1"></div>
<div class="box-2"></div>
<div class="box-3"></div>
# CSS
html, body {
margin: 0;
}
.box-1 {
background: #00F;
bottom: 0;
position: absolute;
width: 100%;
}
.box-2 {
background: #0F0;
height: 30px;
position: relative;
}
.box-3 {
background: #F0F;
height: 66px;
position: relative;
}
# JS
var box1 = $('.box-1');
var box2 = $('.box-2');
var box3 = $('.box-3');
var drawing = false;
var resizeFired = false;
var requestAnimationFrame = window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame;
$(window).resize(function() {
// set resizedFired to true and execute drawResize if it's not already running
if (drawing === false) {
resizeFired = true;
drawResize();
}
});
function drawResize() {
var height;
// render friendly resize loop
if (resizeFired === true) {
resizeFired = false;
drawing = true;
height = $(window).height();
height -= $(box2).height();
height -= $(box3).height();
$(box1).height(height);
requestAnimationFrame(drawResize);
} else {
drawing = false;
}
}
$(window).resize();
Window resize as a user action is real-time event. It fires many times as the user drags.

Categories