user experience

How to build truly responsive images

UPDATE: it looks like Scott Jehl has already beaten me to the punch, so check out his github project!

So, I was almost asleep, and this idea popped into my head. I want to write it down for two reasons:

  1. So I don’t forget it.
  2. To see if anyone else agrees, disagrees, or sees any flaws in the idea.
Basically, I think I have figured out how to make images ridiculously responsive
  1. in a syntax that is logical and 
  2. in a way that can be powered by javascript and 
  3. fails gracefully to non-javascript supporting clients
The code starts out as simple as:
<image data-alt="Some image" data-width="300px" data-height="300px"
  id="my_image" class="some_class">
 
  <source src="/images/high-dpi/image.jpg"
    media="screen and (-webkit-device-pixel-ratio: 2.0)" />
  <source src="/images/print-dpi/image.jpg" media="print" />
  <source src="/images/regular-dpi/image.jpg" />
  <img src="/images/regular-dpi/image.jpg" width="300"
    height="300" alt="Some image" />
</image>

With no javascript, nothing but the <img> tag is displayed, and everything is happy.

With javascript, we run a system which turns it into the following (this becomes easier if we can evaluate media queries in JS, but I’m not sure if we can do that, so I’m showing an alternate way):

<style>
  #my_image {
    background-image: url('/images/regular-dpi/image.jpg');
    background-repeat: no-repeat;
    background-size: 300px 300px;
  }
  @media print {
    #my_image { 
      background-image: url('/images/print-dpi/image.jpg');
    }
  }
  @media screen and (-webkit-device-pixel-ratio: 2.0) {
    #my_image {
      background-image: url('/images/high-dpi/image.jpg');
    }
  }
</style>
<image data-alt="Some image" data-width="300px" data-height="300px"
  id="my_image" style="width: 300px; height: 300px;">
</image>

And now for the fun part – we can use document.getElementById(‘my_image’).style.backgroundImage to get the right image!

This means that the last and final step then turns into:

<image data-alt="Some image" data-width="300px" data-height="300px" id="my_image">
  <img style="width: 300px; height: 300px;" src="/images/high-dpi/image.jpg"
    alt="Some image" width="300" height="300" />
</image>

Which should make it happy with screen readers and other similar systems.

This approach has a few distinct advantages:

  1. It leverages existing tools for defining when to load a particular image (media queries)
  2. It provides a graceful fallback for no javascript support
  3. It (hopefully) will be relatively close to whatever is selected for moving forward in a browser integrated solution, as it fits with the patterns already established.

So that’s it. That’s my master plan. What do you think?

New project offshoots

So, in the never ending endeavor to make things as easy as possible for people to work with, I have a new mini project (actually two!).

majaxInstallerPlugin for symfony (only tested on 1.4.x)

and

MajaxInstaller for everyone else!

MajaxInstaller takes an XML configuration and prompts users interactive questions about the files you describe, to get easy configuration details.

The symfony plugin is just designed to be better integrated into symfony, accepting a yaml config file, and adding a majax:install task

There are no phpunit tests yet in the repository for MajaxInstaller, though I have one or two in majaxInstallerPlugin that actually test the base framework.

I look forward to hearing what people think!

How to build a better date widget in symfony with jQuery

We’re going to step things up a notch with this one, so hang on for the ride, because for those of you not already well versed in symfony and jQuery, there will be a lot of new information, and it will go pretty quickly, but I will try to make sure to keep everyone caught up. This is another post I hinted towards back when I first wrote about symfony widgets. Right, so, on to the fun!

First things first, let’s talk about requirements. My only real requirement is that it must be seamless. We must be able to only implement the widget, and have everything else work exactly as intended. For a date widget, this means when the user clicks that submit button, an array with ‘month’, ‘day’, and ‘year’ must be sent back. Not a string, not JSON, just that basic array.

It’s for the above reason that we will need to make a widget on top of the basic jQuery UI Datepicker Widget. The Datepicker alone would just return a string when we submitted the form, which would then require at least a custom validator, if not much more. So to simplify things later, we’ll put in more effort now.

There are four things we will be doing in this tutorial to make this happen. Here’s a quick overview:

  1. Set up majaxJqueryPlugin to give us our jQuery/jQuery UI base
  2. Set up a new module for us to use with the widget tutorials
  3. Create our widget class
  4. Create our jQuery UI widget that goes along with our widget class

1. Setup majaxJqueryPlugin

We’re using my plugin for two reasons:

  1. It’s my blog, so of course, I’m using my plugin
  2. We need jQuery UI, both the JavaScript and the CSS, and majaxJqueryPlugin provides that right away

Setting it up is easy:

cd plugins
wget -O majaxJqueryPlugin.zip http://jmather.com/wp-content/plugins/download-monitor/download.php?id=4

Now we add the plugin to our Project Configuration. Open up your config/ProjectConfiguration.class.php file, and place this line somewhere within your setup() function:

    $this->enablePlugins('majaxJqueryPlugin');

And one last step, we need to publish the assets:

./symfony plugin:publish-assets

Alright, now we’re ready to move on to the good stuff.

2. Setting up our plugin

Now we need to make a new plugin, and set up a few directories for future use. It’s pretty self-explanatory so we’ll cut right to the code, and come back afterwards to talk about some of the extras.

./symfony generate:plugin majaxWidgetPlugin
mkdir -p plugins/majaxWidgetPlugin/web/js
mkdir -p plugins/majaxWidgetPlugin/lib/widget

We made our plugin, then we made the directory our jQuery plugins will go in, and the directory where we will store our widgets. The -p flag means “no error if existing, make parent directories as needed” which saved us a few mkdir commands.

3. Making our symfony widget

Now open up plugins/majaxWidgetPlugin/lib/widget/majaxWidgetFormDate.class.php and here’s our code:

<?php
 
class majaxWidgetFormDate extends sfWidgetFormDate
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $sfContext = sfContext::getInstance();
    $resp = $sfContext->getResponse();
    $resp->addJavascript('/majaxWidgetsPlugin/js/jquery.majax.dateselector.js');
 
    $id = $this->generateId($name);
 
    $txt = ($this->getOption('can_be_empty') == true) ? 'true' : 'false';
 
    $display = '<div id="'.$id.'"></div>';
    $display .= '<div id="'.$id.'_ctrls">';
    $display .= parent::render($name, $value, $attributes, $errors);
    $display .= '</div>';
    $display .= '
<script type="text/javascript">
$(function() {
  $(\'#'.$id.'\').majaxdateselector({can_be_empty: '.$txt.'});
});
</script>
';
    return $display;
  }
}

So, to start out, we extend sfWidgetFormDate, as that will make the controls we need to manage a date submission. The only change we need to do, is to override the render function, which will let us add some custom code around the standard output, to allow us to more easily control the display.

The first thing we do in the render function is to include the required JavaScript. Some will say the widget shouldn’t know about the JavaScript, but I say, if the widget shouldn’t know… who should? We want to make this as transparent as possible, so why should we have to also remember to add a JavaScript include to use this widget?

Next, we grab our generated ID, and figure out our options (i.e. can the date be null) that we can pass along to Datepicker. After that we wrap the whole deal in a containing div, and then wrap the native controls in another specific div. You will see why we did this in a minute. Lastly, we write the JavaScript to activate the widget.

4. Making our jQuery Plugin

As is the standard, I’ll show you the code, then highlight portions that are interesting. This file goes in plugins/majaxWidgetPlugin/web/js/jquery.majax.dateselector.js

(function($) {
        $.widget('ui.majaxdateselector', {
                version: '1.0.0',
                eventPrefix: 'majax.dateselector',
                options: {
                        can_be_empty: false,
                        datepicker_opts: {
 
                        }
                },
                _create: function() {
                        this.options['id'] = $(this.element).attr('id');
                        this._hide_real_ctrls();
                        this._build_facade();
                        return this;
                },
                _build_facade: function() {
                        $(this.element).html('<input size="10" type="text" id="'+this.options['id']+'_display" />');
                        var tfDisplayUpdate = function(widget) {
                                return function() {
                                        widget._update_ctrls(this.value);
                                }
                        }
 
                        $('#'+this.options['id']+'_display').change(tfDisplayUpdate(this));
 
                        var m, d, y;
                        m = $('#'+this.options['id']+'_month').val();
                        d = $('#'+this.options['id']+'_day').val();
                        y = $('#'+this.options['id']+'_year').val();
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_display').val(this._zero_pad(m, 2)+'/'+this._zero_pad(d, 2)+'/'+y);
                        }
                        $('#'+this.options['id']+'_display').datepicker(this.options['datepicker_opts']);
                        if (this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']).append(' <input type="button" id="'+this.options['id']+'_empty" value="Clear" />');
                                $('#'+this.options['id']+'_empty').button();
                                var tfClear = function(widget) {
                                        return function() {
                                                widget._clear_display();
                                                return false;
                                        }
                                }
                                $('#'+this.options['id']+'_empty').click(tfClear(this));
                        }
                },
                _zero_pad: function(num,count)
                {
                        var numZeropad = num + '';
                        while(numZeropad.length < count) {
                                numZeropad = "0" + numZeropad;
                        }
                        return numZeropad;
                },
                _clear_display: function() {
                        $('#'+this.options['id']+'_display').val('');
                        $('#'+this.options['id']+'_month').val('');
                        $('#'+this.options['id']+'_day').val('');
                        $('#'+this.options['id']+'_year').val('');
                },
                _update_ctrls: function(val) {
                        var vals = val.split('/');
                        if ((val == '' || vals.length != 3) && this.options['can_be_empty'])
                        {
                                $('#'+this.options['id']+'_month').val('');
                                $('#'+this.options['id']+'_day').val('');
                                $('#'+this.options['id']+'_year').val('');
                        }
 
                        var m, d, y;
                        m = vals[0];
                        d = vals[1];
                        y = vals[2];
 
                        if (parseInt(m) > 0 && parseInt(d) > 0 && parseInt(y) > 0)
                        {
                                $('#'+this.options['id']+'_month').val(parseInt(m));
                                $('#'+this.options['id']+'_day').val(parseInt(d));
                                $('#'+this.options['id']+'_year').val(parseInt(y));
                        }
                },
                _hide_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', 'none');
                },
                _show_real_ctrls: function() {
                        $('#'+this.options['id']+'_ctrls').css('display', null);
                },
                destroy: function() {
                        this._show_real_ctrls();
                        ('#'+this.options['id']).html('');
                        $.Widget.prototype.destroy.call(this);
                        return this;
                }
        });
})(jQuery);

You know, looking over, it’s pretty clear, I feel, what most parts do. The functions “_hide_real_ctrls”, and “_show_real_ctrls” hide and show that wrapping div we built around the original controls, so we can keep them in play, but not have to worry about controlling them. The “_build_facade” function builds our fake interactive Datepicker object, and optionally our ‘Clear’ button, if our date is allowed to be empty. Functions “_clear_display” and “_update_ctrls” to exactly as you would expect. Cure functions “_create” and “destroy” are from the jQuery UI Widget framework, and are called … can you guess when? :)

We’re done!

Once you’re done, you can replace any sfWidgetFormDate instance with a majaxWidgetFormDate instance, and everything else is handled. A little bit of effort up front, and many rewards down the road!

Selecting a media player for your project

There are a number of options to consider when selecting a media player for a project. Recently, I had a large media panel to build and which player to use was likely the most important piece of the puzzle.

Here are the factors I used in determining which media player to use:

  • Speed – How fast did it load?
  • Interface – How easy was it to use?
  • API – How easy will it be for me to work with?
  • Documentation – How easy will it be for me to figure out how to work with it?

There are three players I considered primarily. There are many others I looked at, but these three were a clear cut above the rest.

Here’s the break-down of them, and we’ll start with the least compatible for the tasks I was trying to accomplish.

MediaElement.js

In a world moving rapidly towards trying to accomplish 100% cross-platform content compatibility, especially with the recent proliferation of HTML5, new tools are a constant need. MediaElement.js is a giant step forward in this area. It takes the hodgepodge of various presentations throughout the browsers, and consolidates them into one consistent presentation.

The good:

  • Very slick presentation. Allowing the control panel to be completely within the HTML/CSS means you can make any sort of change you need. As I’m planning to add at least one button later, that was a definite bonus.
  • Consistent presentation across platforms.
  • Allows us to use the HTML5 <video> and <audio> tags, to let the latest browsers handle the video natively and then allow us to fall back to other players for less capable browsers.
  • It supports both a fallback Flash and Silverlight player, for maximum ability to ensure video is displayed.

The bad:

  • It was quite slow in my testing. The lag time between when an embedded <video> tag was detected and the video was ready to play, was extremely noticeable. It’s likely also due to loading up and then realize it needs to load a Flash or Silverlight player.
  • It didn’t handle dynamic changes very well. I think this could be solved by integrating it as a jQuery UI Widget as opposed to essentially just a basic wrapper routine.

Flowplayer

What initially impressed me with Flowplayer was the promise it showed for extending the base display with html overlays to help add contextual information to the video. Then I started looking over the site, and noticed that the documentation is absolutely top notch. These guys aren’t playing around. Very impressive.

The good:

  • The documentation. Oh my … word.
  • The API. Not only is it very accessible and well thought out, it’s throughly documented.
  • Playlists. It’s something I missed when playing with MediaElement.js.
  • Have I mentioned that Flowplayer is well documented.

The bad:

  • It’s huge. At roughly two times the size of JW Player, Flowplayer is the heavyweight of the bunch, which slows down that all important first page-load impression.
  • There’s some weirdness in it’s playlist system. When you push in a new playlist, the “cover image” isn’t loaded until you run play, which adds more delay between when the user initiates an action and when the player is where I will call “ready”.

JW Player

JW Player is now on version 5.2. I first used it somewhere back in version 3, and as a disclosure, own an unlimited license for both versions 3 and 4. I have been consistently impressed by the flexibility of the player, and it’s ability to handle whatever I throw at it… once you figure out how to tell it what it wants to know.

The good:

  • Flexibility is the name of the game.
  • It did everything I wanted it to, in a way that I could live with.
  • It was lightweight and the quickest to go from page load to presentation.

The bad:

  • The documentation could use some work. Instead of having answers at my finger tips, I had to search for them. Even for pretty basic things.
  • Some more descriptive errors might be nice, as opposed to things simply not working.

In the end, JW Player worked best for my purposes on this project, but every project is different, and I hope this helps some of you save some time.

Using JavaScript Responsibly

Since we’ve started the conversation, let’s take a step back and talk about something a little less fun, but just as important: when is it okay to require JavaScript use?

My hard and fast rule is that anywhere a login is required to reach, you can require JavaScript. If it’s publicly accessible, however, you should do everything possible to avoid requiring JavaScript. It takes more work to have a JavaScript and non-JavaScript version, but it makes sure your site remains accessible.

twitter