f w h

PSA: Viewport Units on SVG

A quick note, learned from experience: viewport units (vw, vh, vmin, vmax) do not work as height or width attributes in Firefox. I tested the latest (as of this post’s pub date) versions of: Chrome, Firefox, Edge and IE11. It seems to work perfectly in all of those except Firefox.

What do I mean? Take a look:


<svg height="10vh" width="10vw">
	<!-- SVG innards -->
</svg>

This works with no issue in Chrome most browsers, but will be ignored in Firefox. Why? I’m guessing it’s Firefox sticking closely to the spec, as Mozilla doesn’t list them as an available option in their MDN document on SVG sizes, and neither does the W3C spec for the length data-type on SVGs.

Fret not, however, because I have a fix for it.

Viewport units are simply a percentage of the viewport’s, or as the CSS spec states, ‘initial containing block’ size. Thus, if your viewport is 1000px wide, 1vw is equivalent to 10px. The only problem is that it will need to adjust automatically when the viewport changes.

The script below sets the viewport units aside in a data-* attribute, then uses them to calculate a pixel value for them:


// My viewport script from a previous post
function viewport(){

	let w = window.innerWidth;
	let h = window.innerHeight;
	
	let orientation = w > h ? 'landscape' : 'portrait';
	
	let output = {
		w: w,
		h: h,
		orientation: orientation
	};

	return output;

}

// Converts the viewport units to a pixel-based length
function viewport2px(size){

	let int = parseInt(size, 10); // Grabs the size and extracts only the number
	let output;
	
	size.includes('h') ? output = ((int / 100) * viewport().h) + 'px' :  output = ((int / 100) * viewport().w) + 'px';
	
	return output;

}

// Sets the new units according to their current size
function sizeSVGs(svgsIn){

	// If a NodeList object is not provided, just select all of them
	let svgs = svgsIn || document.querySelectorAll('svg');
	
	Array.from(svgs).forEach(function(svg){
		// Check the height for viewport units (doesn't check for vmin/vmax - easy enough to add),
		// Sets those units aside for later use,
		// Sets the new px size
		if(svg.getAttribute('height').includes('vw') || svg.getAttribute('height').includes('vh')){
			svg.setAttribute('data-height', svg.getAttribute('height'));
			svg.setAttribute('height', viewport2px(svg.getAttribute('height')));
		}
		// Does the same thing for width
		if(svg.getAttribute('width').includes('vw') || svg.getAttribute('width').includes('vh')){
			svg.setAttribute('data-width', svg.getAttribute('width'));
			svg.setAttribute('width', viewport2px(svg.getAttribute('width')));
		}
	});

}

// Fire on load
window.addEventListener('load', function(){

	sizeSVGs();

});

But what about the resize? Well, remember how we set aside the initial values in a data-*attribute? We just need to grab that value again and recalculate any time a resize event fires.


window.addEventListener('resize', function(){
	Array.from(svgs).forEach(function(svg){
		if(svg.hasAttribute('data-height')) svg.setAttribute('height', vp2px(svg.getAttribute('data-height')));
		if(svg.getAttribute('data-width')) svg.setAttribute('width', vp2px(svg.getAttribute('data-width')));
	});
});

Caveat: This script uses the Array.from()method to convert the SVG NodeList into an array for iteration. No version of IE supports this method. If IE support is required, MDN has a great drop-in polyfill.

WordPress Development Environment Scripts

It is useful to have certain items or scripts available to you while developing that you may not want present in production. Forgetting to remove these items before you go to production can be dangerous.

When developing with WordPress, a short script in your functions.php can add these tools when you’re developing locally, but hide them when live.

Create your scripts in a separate file in the js directory of your theme. I called mine dev.js. Then, add the following to your functions.php file:


function add_dev_class( $classes ) {
 
    $classes[] = 'dev';
     
    return $classes;
     
}

function enqueue_dev(){

    wp_register_script( 'Dev', get_stylesheet_directory_uri() . '/js/dev.js', [ 'jquery' ], NULL, true );
    wp_enqueue_script( 'Dev' );

}

// Here comes the magic
if( $_SERVER[ 'SERVER_NAME' ] == "localhost" ){

    add_filter( 'body_class', 'add_dev_class' );
    add_action( 'wp_enqueue_scripts', 'enqueue_dev' );

}

The first function will add a class called “dev” to the body tag, so you can hook it with JavaScript or add development-centric CSS styles.

The second function will enqueque the dev.js script file.

Finally, the conditional at the end will check what the current base address is and if it matches, will execute the functions. I’ve used localhost here, but you can use whatever your development domain is.

I use this to load a variation of my viewport size bookmarklet that sits at the bottom of the window with a plethora of information to help inform my decisions when designing for responsiveness.

Mark Your Spot on a Page – Bookmarklet

Recently, I was trying to work my way through an HTML-based online book. It became a pain to find my spot again when I had to close the browser or reboot the computer for various reasons. So, I created a bookmarklet to help me find my spot again when I return to the page. The bookmarklet adds a small box in the upper right corner of the window with buttons to add or clear bookmarks. When you click the ADD button, you’ll be prompted to name the bookmark, which will then be added to the list. Bookmarks are stored in local storage, so they will persist from session to session. You’ll just have to click the bookmarklet again to reload them.

Go HERE to get the bookmarklet. Full code is below:


function bm_go(){

  var bm_block;

  bm_block = document.createElement('div');
  bm_block.classList.add('bm_class');
    bm_block.style.width = '100px';
    bm_block.style.minHeight = '33px';
    bm_block.style.position = 'fixed';
    bm_block.style.padding = '5px';
    bm_block.style.top = '10px';
    bm_block.style.right = '10px';
    bm_block.style.backgroundColor = 'white';
    bm_block.style.fontFamily = 'Arial, sans-serif';
    bm_block.style.color = 'black';
    bm_block.style.borderRadius = '5px';
    bm_block.style.border = '1px solid #999';
    bm_block.style.zIndex = '10000000';

  var bm_add_button = document.createElement('div');
  	bm_add_button.classList.add('bm_add_button');
    bm_add_button.style.display = 'inline-block';
    bm_add_button.style.height = '33px';
    bm_add_button.style.width = '45%';
    bm_add_button.style.border = '1px solid #999';
    bm_add_button.style.fontSize = '15px';
    bm_add_button.style.textAlign = 'center';
    bm_add_button.style.lineHeight = '33px';
    bm_add_button.style.cursor = 'pointer';
    bm_add_button.style.boxShadow = '2px 2px #666';
    bm_add_button.innerText = 'ADD';
  bm_block.appendChild(bm_add_button);

  var bm_clr_button = document.createElement('div');
  	bm_clr_button.classList.add('bm_clr_button');
    bm_clr_button.style.display = 'inline-block';
    bm_clr_button.style.height = '33px';
    bm_clr_button.style.width = '45%';
    bm_clr_button.style.border = '1px solid #999';
    bm_clr_button.style.fontSize = '15px';
    bm_clr_button.style.textAlign = 'center';
    bm_clr_button.style.lineHeight = '33px';
    bm_clr_button.style.cursor = 'pointer';
    bm_clr_button.style.boxShadow = '2px 2px #666';
    bm_clr_button.style.marginLeft = '5%';
    bm_clr_button.innerText = 'CLR';
  bm_block.appendChild(bm_clr_button);

  var bm_message = document.createElement('div');
  	bm_message.classList.add('bm_message');
    bm_message.style.marginTop = '5px';
    bm_message.style.marginBottom = '5px';
    bm_message.style.fontSize = '10px';
    bm_message.style.textAlign = 'center';
    bm_message.innerText = 'Click "ADD" to set bookmark at current scroll position.';
  bm_block.appendChild(bm_message);
    
  	document.body.appendChild(bm_block);
  
  var bm_list_init = JSON.parse(localStorage.getItem('bm_list')) || [];
   
  function bm_pop_list(list){
  	
    var rm_bm = document.querySelector('.bm_bm_list');
    if (rm_bm) rm_bm.parentNode.removeChild(rm_bm);
    
    var bm_bm_list = document.createElement('ul');
    bm_bm_list.classList.add('bm_bm_list');
    bm_bm_list.style.paddingLeft = '0';
    
    for (var i = 0; i < list.length; i++){

      var bm_list_item = document.createElement('li');
      bm_list_item.classList.add('bm_list_item');
      bm_list_item.innerText = list[i].name;
      bm_list_item.setAttribute('data-x', list[i].x);
      bm_list_item.setAttribute('data-y', list[i].y);
      bm_list_item.style.marginLeft = '0';
      bm_list_item.style.listStyleType = 'none';
      bm_list_item.style.cursor = 'pointer';

      bm_list_item.addEventListener('click', function(){

      	bm_go_to_location(bm_list_item);

      });

      bm_bm_list.appendChild(bm_list_item);

    }
    
    bm_block.appendChild(bm_bm_list);

  }
  
  bm_pop_list(bm_list_init);
  
  function bm_go_to_location(item){

  	window.scrollTo(item.getAttribute('data-x'), item.getAttribute('data-y'));

  }
  
  function bm_add_item(){

  	var bm_get_name = prompt('Enter Bookmark Name', 'bookmark'),
      	bm_get_x = window.pageXOffset,
        bm_get_y = window.pageYOffset;
    var bm_list = JSON.parse(localStorage.getItem('bm_list')) || [];

    bm_list.push({
    	name: bm_get_name,
      x: bm_get_x,
      y: bm_get_y
    });

    localStorage.setItem('bm_list', JSON.stringify(bm_list));
    bm_pop_list(bm_list);

  }
  
  function bm_clear(){

  	localStorage.removeItem('bm_list');
    bm_pop_list([]);

  }
  
  bm_add_button.addEventListener('click', bm_add_item);

  bm_clr_button.addEventListener('click', bm_clear);
  
}

var bm_ = new bm_go();

Display Viewport Size Bookmarklet

Chrome has a nice little overlay shows the viewport size when you resize if you have the developer tools open. For whatever reason, it stopped working on me for a short while. It was a bit frustrating, because of course it would decide not to work right when I needed it most.

So, why not recreate the magic?

Get the bookmarklet here

Original, uncompressed code below:


function vp_getViewport(){
  this.w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
  this.h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);

  this.output = {
    w: this.w,
    h: this.h
  };

  return this.output;
}
function vp_init(){
  var e = document.createElement('div');
  e.classList.add('vp_vpElement');
  e.style.backgroundColor = '#aaa';
  e.style.border = '1px solid black';
  e.style.padding = '2px';
  e.style.fontSize = '12px';
  e.style.position = 'fixed';
  e.style.top = '10px';
  e.style.right = '10px';
  e.style.zIndex = '1000000';
  document.body.appendChild(e);
}
vp_init();
function vp_showSize(viewport){
  var e = document.querySelector('.vp_vpElement');
  e.innerHTML = viewport.w + ' x ' + viewport.h;
}
window.addEventListener('load', function(){vp_showSize(vp_getViewport());});
window.addEventListener('resize', function(){vp_showSize(vp_getViewport());});

Simple Mobile Detection for JavaScript

One common question I see asks how one might conditionally run JavaScript based on screen size. There are a number of use cases for this, and there are a couple ways to go about it.
One easy way, as I wrote about almost exactly three years ago is to detect a CSS property based on a media query.
Another possibility is to use JavaScript to pull the size of the browser’s viewport. To make things easy, we can package everything in a neat little function that returns an object with the viewport details.


function viewport(){
  this.w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
  this.h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
  
  this.orientation = this.w > this.h ? 'landscape' : 'portrait';

  this.output = {
    w: this.w,
    h: this.h,
    orientation: this.orientation
  };

  return this.output;
}

The function can then be used in a number of ways.



// Conditionally do a thing inside a function
function doAThing(){
  if(viewport().w < 480){
    // Do a thing
  }
}

// Do a thing when the browser is resized
window.onresize = function(){
  if(viewport().orientation == 'portrait'){
    // Do a thing
  }
}

UPDATE July 2017: JS matchMedia API

Recently, SitePoint updated one of their 2011 posts to recommend this API.
In short, matchMedia returns a mediaQueryList object, which has a .matches() method attached to it. Passing a media query formatted the same as a CSS media query will return a boolean based on whether the query matches.
Read more about it on SitePoint’s How to use Media Queries in JavaScript.

Next Page »