Code & Web Design

Forcing Fonts to Fit with jQuery
Dec
27
2012

I honestly don’t know how I used to code Javascript before jQuery. Though I’ve tried to repress the horrible memories of those days, I still suffer from flashbacks of hunting through obscure alert errors messages to determine which browser was choking on my code… and that was just to do simple menu mouseover animations!

Thankfully things have come a very long way in the last few years! We now have free access to excellent Javascript debugging tools such as Firefox Firebug and Chrome Developer Tools, slick APIs such as jQuery that put the focus back on writing powerful code, and an emerging cross-browser convergence towards true standards compliance (though Internet Explorer still lives its own fantasy world where standards are mere suggestions, much to the agony of developers worldwide).

Anyway, on to the code…

A common problem in web design is forcing a variable-length text string to fit inside a fixed-dimension container. For example, consider this 200px by 30px box:

200px by 30px
<style>.my-box { text-align: center; width: 200px; height: 30px; background: #eeeeee; border: 1px solid #666; margin: 10px; }</style>
<div class="my-box">200px by 30px</div>

Assume that this box needs to stay fixed at 200px by 30px in order to fit into your website layout. The string inside this box will be set to the title to the page you are currently on. This page title string can be any length and may or may not contain white spaces.

As you can see, this will quickly become a problem if we have very long words or strings in the fixed container:

ThisIsAVeryLongWordWithoutAnyWhitespace
This Is A Very Long String With Whitespace

Before jQuery, we only had two options to address this problem:

  1. Limit the maximum length of all our titles to fit within the space, or
  2. Use the CSS property overflow: hidden to hide the extra text

The Solution

Neither option really solves the problem of making the text fit inside the box. Thankfully, jQuery gives us the power to “clean up” the the text in the box as soon as the page loads. Once $(document).ready() fires and we know the dimensions of the target container, we can call my custom function $.fn.fitFont() to shrink the CSS font-size property until the string fits inside.

Here are the two previous examples with $.fn.fitFont() applied:

ThisIsAVeryLongWordWithoutAnyWhitespace
This Is A Very Long String With Whitespace
$('#my-box1, #my-box2').fitFont({width: 200, height: 30});

The Details

$.fn.fitFont() takes a single JSON object opts as a parameter. opts can have up to four parameters:

  1. width – (int) width in px of the target container
  2. height – (int) height in px of the target container
  3. target – (Object) jQuery object of the target container
  4. minsize – (int) minimum size of the font

NOTE: either target OR width/height are required. minsize is optional.

$.fn.fitFont() works as follows:

  1. The user-defined options in opts overrides the function defaults:
    var settings = {
    	width: 0,
    	height: 0,
    	target: null,
    	minsize: 6
    };
    $.extend(settings, opts);
    
  2. The target dimensions are set based on either the specified width/height or the inner dimensions of the specified target element:
    var cw = settings.width;
    var ch = settings.height;
    if (settings.target) {
    	cw = parseInt(settings.target.innerWidth());
    	ch = parseInt(settings.target.innerHeight());
    }
    
  3. A hidden <DIV> is created off-screen by using the CSS property left: -9999px. This <DIV> contains a copy of the text string and is assigned the same font properties as the original:
    var elem = $(this);
    var text = elem.html();
    var html = $('<span style="postion:absolute;width:auto;left:-9999px">' + text + '</span>');
    html.css({"font-family": elem.css("font-family"), "font-size": elem.css("font-size"), "line-height": elem.css("line-height")});
    $('body').append(html);
    
  4. The width/height of the hidden <DIV> is measured. While the width/height of the hidden <DIV> is larger than the target dimensions, we shrink the font by one pixel and remeasure the hidden <DIV>. The font continues to shrink until the hidden <DIV>‘s dimensions fit within the target dimensions:
    var ew = html.width();
    var eh = html.height();
    var fSize = parseInt(elem.css('font-size'));
    
    if (ew && eh && cw && ch) {
    	// shrink font until it fits with specified dimensions
    	while ((fSize > settings.minsize) && ((ew > cw) || (eh > ch))){
    		fSize--;
    		html.css({'font-size': fSize+'px'});
    		ew = html.width();
    		eh = html.height();
    	}
    }
    
  5. Finally, we set the CSS font-size property of the original element and remove the copied hidden <DIV> from the DOM:
    elem.css({'font-size': fSize+'px'});
    html.remove();
    

The Code

Here’s the function in its entirety:

/**
 * Shrinks the font-size of an element until the entire text string fits inside the specified dimensions
 *
 * @param	settings	JSON	JSON object containing the following arguments:
 * 							 { 	width		integer		target width dimension
								height		integer		target height dimension
								target		Object		jQuery DOM object
								minsize		integer		minimum font size
 *
 */
$.fn.fitFont = function(opts)
{
	var settings = {
		width: 0,
		height: 0,
		target: null,
		minsize: 6
	};
	$.extend(settings, opts);
	
	var cw = settings.width;
	var ch = settings.height;
	if (settings.target) {
		cw = parseInt(settings.target.innerWidth());
		ch = parseInt(settings.target.innerHeight());
	}
	
	var elem = $(this);
	var text = elem.html();
	var html = $('<span style="postion:absolute;width:auto;left:-9999px">' + text + '</span>');
	html.css({"font-family": elem.css("font-family"), "font-size": elem.css("font-size"), "line-height": elem.css("line-height")});
	$('body').append(html);
	
	var ew = html.width();
	var eh = html.height();
	var fSize = parseInt(elem.css('font-size'));
	
	if (ew && eh && cw && ch) {
		// shrink font until it fits with specified dimensions
		while ((fSize > settings.minsize) && ((ew > cw) || (eh > ch))){
			fSize--;
			html.css({'font-size': fSize+'px'});
			ew = html.width();
			eh = html.height();
		}
	}
	elem.css({'font-size': fSize+'px'});
	html.remove();
};

Add get_comments_popup_link to WordPress
Dec
16
2012

WordPress is a great platform for building your own blog, but, as with any open-source code base, it has a few annoyances that makes writing your own custom code harder than should be necessary. One of my biggest complaints has to be the inconsistency with functions that return strings versus output the string via echo.

Most major functions in WordPress have echo and string variants. For instance, we can output the title of a WordPress post with:

<?php the_title(); ?>

Alternatively, we can store the value of the post title as a string variable with:

<?php $str = get_the_title(); ?>

As I said, most major functions in WordPress follow this pattern— but an annoyingly small set only have their echo variant. This is the case for the function comments_popup_link, which echoes out a formatted link to the comments section of a post. Unfortunately, WordPress does not have a get_comment_popup_link counterpart, which was exactly what I needed when building my Read More and Comments links for TheScubaGeek.com.

I copy/pasted the core functionality for comments_popup_link into my theme’s functions.php file, then modified the echo statements to write to a string instead. I also had to modify comments_number to be a string variant function get_comments_number_str (the function get_comments_number does exists in WordPress, but it does not apply the string formatting options).

Pretty simple copy/paste/replace job, but I figured I’d share this with the community to save everyone future headaches:

/**
 * Modifies WordPress's built-in comments_popup_link() function to return a string instead of echo comment results
 */
function get_comments_popup_link( $zero = false, $one = false, $more = false, $css_class = '', $none = false ) {
	global $wpcommentspopupfile, $wpcommentsjavascript;

	$id = get_the_ID();

	if ( false === $zero ) $zero = __( 'No Comments' );
	if ( false === $one ) $one = __( '1 Comment' );
	if ( false === $more ) $more = __( '% Comments' );
	if ( false === $none ) $none = __( 'Comments Off' );

	$number = get_comments_number( $id );

	$str = '';

	if ( 0 == $number && !comments_open() && !pings_open() ) {
		$str = '<span' . ((!empty($css_class)) ? ' class="' . esc_attr( $css_class ) . '"' : '') . '>' . $none . '</span>';
		return $str;
	}

	if ( post_password_required() ) {
		$str = __('Enter your password to view comments.');
		return $str;
	}

	$str = '<a href="';
	if ( $wpcommentsjavascript ) {
		if ( empty( $wpcommentspopupfile ) )
			$home = home_url();
		else
			$home = get_option('siteurl');
		$str .= $home . '/' . $wpcommentspopupfile . '?comments_popup=' . $id;
		$str .= '" onclick="wpopen(this.href); return false"';
	} else { // if comments_popup_script() is not in the template, display simple comment link
		if ( 0 == $number )
			$str .= get_permalink() . '#respond';
		else
			$str .= get_comments_link();
		$str .= '"';
	}

	if ( !empty( $css_class ) ) {
		$str .= ' class="'.$css_class.'" ';
	}
	$title = the_title_attribute( array('echo' => 0 ) );

	$str .= apply_filters( 'comments_popup_link_attributes', '' );

	$str .= ' title="' . esc_attr( sprintf( __('Comment on %s'), $title ) ) . '">';
	$str .= get_comments_number_str( $zero, $one, $more );
	$str .= '</a>';
	
	return $str;
}

/**
 * Modifies WordPress's built-in comments_number() function to return string instead of echo
 */
function get_comments_number_str( $zero = false, $one = false, $more = false, $deprecated = '' ) {
	if ( !empty( $deprecated ) )
		_deprecated_argument( __FUNCTION__, '1.3' );

	$number = get_comments_number();

	if ( $number > 1 )
		$output = str_replace('%', number_format_i18n($number), ( false === $more ) ? __('% Comments') : $more);
	elseif ( $number == 0 )
		$output = ( false === $zero ) ? __('No Comments') : $zero;
	else // must be one
		$output = ( false === $one ) ? __('1 Comment') : $one;

	return apply_filters('comments_number', $output, $number);
}

Armed with the new string-returning functions above, I was able to modify WordPress’s excerpt_more filter in my theme’s functions.php file to append the following links at the end of each post excerpt.

function new_excerpt_more($more) {
	$str = ' [...]<br /><a href="'.get_permalink().'" class="read-more">Read More</a>';
	$str .= get_comments_popup_link('Add Comment', '1 Comment ', '% Comments ', 'read-comments', 'Comments off');
	return $str;
}
add_filter('excerpt_more', 'new_excerpt_more');

TheScubaGeek Reloaded
Dec
12
2012

Wow. It’s been a while. And by a while, I mean a really long time. But here I go again, blowing the dust off the keyboard, cracking my knuckles, and digging in for some serious blogging.

I’m in the middle of doing some HUGE upgrades to TheScubaGeek.com right now…. please hang on while I get the last of the finishing touches together. You’ll find busted links, broken images, and all sorts of clutter around here for the next two weeks. In order to rebuild TheScubaGeek.com into the site it needs to be, I have to manhandle WordPress into submission and completely retrofit my old content from Roatan into this new vision. But trust me, it will be worth it.

It’s gonna get crazy…
Jul
25
2010

I know this site has been quiet for a bit. I’ve been busy recovering from a body surfing wipe-out and programming like mad to get a few web contracts wrapped up.

WordPress, the blogging platform used to power TheScubaGeek.com and my other sites, recently released some big changes in version 3.0. I’ve been hacking through the code for the last two months for another project and have grown quite fond of some of its hidden features. The WordPress open source code base is still definitely a “point-oh” (lots of undocumented code and some sloppy implementation in parts), but wow— definitely some big improvements!

I’m going to be porting TheScubaGeek.com to my newest engine based on WordPress 3.0 over the next few days. There may be some interruptions, so bear with me and the mess!

Lia Barrett Photography goes live
May
19
2010

The phenomenal underwater and travel photography of Lia Barrett now has a new home on the web at http://www.liabarrettphotography.com.

Lia and I go back a few years when I was a scuba diving instructor at Coconut Tree Divers on the island of Roatan, Honduras. When I first met her, she was helping film the hilariously disastrous Roatan Movie— the making which was infinitely funnier than the final result. We later collaborated on photo shoots for a few web projects around the island.

Lia probably holds the world record for most time spent inside a homemade submarine (not including Karl Stanley and Barry, of course). For theses images, she was crouched for hours in a tiny spherical dome. She had to keep her lens close to the mere five inches of convex glass separating her thousands of pounds of crushing pressure— but not too close or the cold condensation dripping from the ceiling would fry her camera. She had to wait— and wait— and wait until the right deep sea creature swam by, then try to snap off quality shots with both the submarine and the creature in motion. The results are nothing short of incredible.

Lia has since explored the seas and land of Asia and the South Pacific. She is currently in Australia.