Build a unique selector for any DOM element using jQuery

by timvasil 2/24/2014 7:19:00 PM

If you need a selector that uniquely identifies a DOM element and is robust to some changes to the page structure over time, your best bet is to keep the selector as concise as possible.  I've seen jQuery plugins that attempt to build a selector by navigating the entire DOM hierarchy, and adding some unique bits for each ancestor encountered along the way.  This can be overkill -- especially if any element has an ID along the way.  Since ID is unique, there's no need to keep going.

Here's a custom jQuery function called "getSelector" to provide a concise selector for any element on the page.

$.fn.getSelector = function() {
  var el = this[0];
  if (!el.tagName) {
    return '';
  }

  // If we have an ID, we're done; that uniquely identifies this element
  var el$ = $(el);
  var id = el$.attr('id');
  if (id) {
    return '#' + id;
  }

  var classNames = el$.attr('class');
  var classSelector;
  if (classNames) {
    classSelector = '.' + $.trim(classNames).replace(/\s/gi, '.');
  }

  var selector;
  var parent$ = el$.parent();
  var siblings$ = parent$.children();
  var needParent = false;
  if (classSelector && siblings$.filter(classSelector).length == 1) {
     // Classes are unique among siblings; use that
     selector = classSelector;
  } else if (siblings$.filter(el.tagName).length == 1) {
     // Tag name is unique among siblings; use that
     selector = el.tagName;
  } else {
     // Default to saying "nth child"
     selector = ':nth(' + $(this).index() + ')';
     needParent = true;
  }

  // Bypass ancestors that don't matter
  if (!needParent) {
    for (ancestor$ = parent$.parent();
         ancestor$.length == 1 && ancestor$.find(selector).length == 1;
         parent$ = ancestor$, ancestor$ = ancestor$.parent());
    if (ancestor$.length == 0) {
       return selector;
    }
  }

  return parent$.getSelector() + ' > ' + selector;
}

To test this on your page, you can try out this code -- it'll write the selector to the console wherever you click.

$("*").click(function() {
        var selector = $(this).getSelector();
        console.log(selector + ' --> matches ' + $(selector).length + ' element');
        return false;
      });

Here's some example output (notice that many DOM elements, including HTML and BODY, are excluded because they are not necessary):

#recentPosts > :nth(0) > A --> matches 1 element

Tags:

JavaScript | jQuery

How to convert an SVG image into a static image with only JavaScript

by timvasil 2/6/2014 1:15:00 AM

Recently I was trying to convert a Highcharts SVG-based chart into a static image that I could copy and paste into a document.  Highcharts provides a mechanism to do this on with a server-side roundtrip, but for modern browsers (and, no, not any flavor of IE), it's possible to do this completely on the client side -- and even inline replace the SVG element with a pixel-perfect matching <img> element.

Here's the code (written as a jQuery extension):

    /**
     * Converts an SVG element to an IMG element with the same dimensions
     * and same visual content. The IMG src will have a temporary blob URL.
     * Works in Webkit and Firefox and IE10+.
     */
    $.fn.toImage = function() {
        $(this).each(function() {
            var svg$ = $(this);
            var width = svg$.width();
            var height = svg$.height();

            // Create a blob from the SVG data
            var svgData = new XMLSerializer().serializeToString(this);
            var blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });

            // Get the blob's URL
            var blobUrl = (self.URL || self.webkitURL || self).createObjectURL(blob);

            // Load the blob into an image
            $('<img />')
                .width(width)
                .height(height)
                .on('load', function() {
                    // Overwrite the SVG tag with the img tag
                    svg$.replaceWith(this);
                })
                .attr('src', blobUrl);
        });
    };

If you'd like the destination tag to have Base64-encoded data rather than a temporary blob URL and/or want to manipulate the image (e.g. set the background color), add an intermediate step of rendering the image on a canvas first. Unfortunately this step doesn't work in IE, whose security model marks the canvas as "write only" once SVG data is written to it, regardless of where it came from.

    /**
     * Converts an SVG element to an IMG element with the same dimensions
     * and same visual content. The IMG src will be a base64-encoded image.
     * Works in Webkit and Firefox (not IE).  
     */
    $.fn.toImage = function() {
        $(this).each(function() {
            var svg$ = $(this);
            var width = svg$.width();
            var height = svg$.height();

            // Create a blob from the SVG data
            var svgData = new XMLSerializer().serializeToString(this);
            var blob = new Blob([svgData], { type: "image/svg+xml;charset=utf-8" });

            // Get the blob's URL
            var domUrl = self.URL || self.webkitURL || self;
            var blobUrl = domUrl.createObjectURL(blob);

            // Load the blob into a temporary image
            $('<img />')
                .width(width)
                .height(height)
                .on('load', function() {
                    try {
                        var canvas = document.createElement('canvas');
                        canvas.width = width;
                        canvas.height = height;
                        var ctx = canvas.getContext('2d');

                        // Start with white background (optional; transparent otherwise)
                        ctx.fillStyle = '#fff';
                        ctx.fillRect(0, 0, width, height);

                        // Draw SVG image on canvas
                        ctx.drawImage(this, 0, 0);

                        // Replace SVG tag with the canvas' image
                        svg$.replaceWith($('<img />').attr({
                            src: canvas.toDataURL(),
                            width: width,
                            height: height
                        }));
                    } finally {
                        domUrl.revokeObjectURL(blobUrl);
                    }
                })
                .attr('src', blobUrl); 
        });
    };

Usage is straightforward. For example, to convert all the SVG images on a page to static images, do this:

$('svg').toImage();

Tags:

Highcharts | JavaScript | jQuery | SVG

Workaround for WebKit bug rendering with "text-transform: uppercase"

by timvasil 1/30/2014 12:11:00 AM

There's a longstanding WebKit bug whereby DOM elements styled with "text-transform: uppercase" don't have their metrics (height, width) updated when manipulated dynamically, e.g., $('selector').html('new content');

The problem is documented here:  https://bugs.webkit.org/show_bug.cgi?id=99606

The simple fix I've found is to do this:  $('selector').hide().show();

Tags:

JavaScript | jQuery | WebKit

Fixing bad behaviors with IIS's reverse proxy: it's all about the pipeline mode

by timvasil 11/20/2013 11:53:00 AM

I noticed an "gotcha" when using the IIS reverse proxy feature (ARR -- attribute request routing):  it won't work well if the app pool doing the URL rewriting is using Integrated pipeline mode.  

Bad behaviors I saw in integrated mode included:

  1. "502.3 - Bad Gateway The operation timed out" errors after the proxy timeout period expired (2 minutes by default).  This typically happened with a subset of POST requests and 302 (redirect) responses. 
  2. "Unexpected end of MIME multipart stream. MIME multipart message is not complete".  This happened for POST requests with multipart/form-data content (i.e. file uploads).
  3. Increased latency.

The fix is to change the app pool's pipleine mode to Classic.

Tags:

ASP.NET | IIS | ARR

jQuery extension method to parse a query string

by timvasil 3/21/2013 9:49:00 PM

This code automatically trims a leading "?" (if present) and accomodates multiple params with the same key by stuffing them into an array.  (You can also find it on Gist.)

(function($) {
    var re = /([^&=]+)=?([^&]*)/g;
    var decode = function(str) {
        return decodeURIComponent(str.replace(/\+/g, ' '));
    };
    $.parseParams = function(query) {
        var params = {}, e;
        if (query) {
            if (query.substr(0, 1) == '?') {
                query = query.substr(1);
            }

            while (e = re.exec(query)) {
                var k = decode(e[1]);
                var v = decode(e[2]);
                if (params[k] !== undefined) {
                    if (!$.isArray(params[k])) {
                        params[k] = [params[k]];
                    }
                    params[k].push(v);
                } else {
                    params[k] = v;
                }
            }
        }
        return params;
    };

Example usage:


  $.parseParams(document.location.search)
 

Tags:

JavaScript | jQuery

Pagination JavaScript

by timvasil 3/20/2013 8:25:00 PM

Before I settled on an "infinite scrolling" solution to a pagination problem, I was using Twitter Bootstrap's "pagination" CSS class and a bunch of page links, including the usual next/previous.  I wrote the script to meet these requirements:

  1. First and previous page links must always be available
  2. Next and last page links must always be available
  3. The current page must be highlighted and non-clickable
  4. Up to X additional links must be visible for interstitial pages before and after the current page.  
  5. Each link must consume the same amount of horizontal space so next/previous/first/last links don't change position.  This prevents accidental clicks if the user were to click multiple times and the elements jumped around.

Here are some examples of how this looks for various pages:

This is the code:

var html = [];
var maxBlocks = 11;  // specify the max <li> elements you want rendered
var currentPage = 1; // specify current page here
var numPages = Math.ceil(totalItems / pageSize);

if (numPages > 0) {
    addPageLink = function(page, label, tooltip) {
        var cls = (page == currentPage || page === null) ? 'disabled' : '';
        if (label == currentPage) {
            cls += ' active';
        }
        html.push('<li title="', tooltip, '" data-page="', page, '" class="', cls, 
             '"><a href="#">', label, '</a></li>');
    }
    
    html.push('<ul>');
    addPageLink(Math.max(1, currentPage - 1), '&laquo;', 'Previous page');
    addPageLink(1, 1, 'First page');

    var maxPivotPages = Math.round((maxBlocks - 5) / 2);
    var minPage = Math.max(2, currentPage - maxPivotPages);
    var maxPage = Math.min(numPages - 1, 
                           currentPage + maxPivotPages * 2 - (currentPage - minPage));
    minPage = Math.max(2, minPage - (maxPivotPages * 2 - (maxPage - minPage)));

    for (var i = minPage; i <= maxPage; i++) {
        var isMore = (i == minPage && i != 2) || (i == maxPage && i != numPages - 1);
        if (isMore) {
            addPageLink(null, '&hellip;');
        } else {
            addPageLink(i, i, 'Page ' + i);
        }
    }
            
    addPageLink(numPages, numPages, 'Last page');
    addPageLink(Math.min(numPages, currentPage + 1), '&raquo;', 'Next page');
    html.push('</ul>');
}

$('.pagination').html(html.join(''));

And here are the Bootstrap CSS tweaks to get fixed width and the highlighted state (in Less syntax):

.pagination {
    li a {
        min-width: 20px;
    }
    
    li.active {
        a, a:hover, a:active {
            background-color: #005580;
            color: #fff;
        }
    }
}

Tags:

CSS | JavaScript | jQuery | Twitter Bootstrap

Options for a multi-app large-screen kiosk

by timvasil 3/19/2013 6:05:00 PM

I've been looking for a way to build a large-screen kiosk capable of running multiple apps and websites, but still run in a 'locked down" mode so kiosk users can't escape the sandbox and do something malicious.  The iPad now has a kiosk mode, but it limits you to a single app.  Android has kiosk apps, like SureLock, that do support multiple apps and websites, however there's no Android device quite large enough for what I'm trying to do (the 24" ViewSonic is the largest I've seen).  That leaves a custom solution:  probably built on top of Windows so Windows apps + Android apps (in an emulator like BlueStacks).

Here's a summary of the options I've evaluated to date to get a ~40" multi-app exhibit/kiosk experience:

 

Option 1 

Option 2 

Option 3

Concept

10 iPads + Mac Mini

Android all-in-one device

Large touchscreen + PC

Screen size

9.7" x 10 devices

24" x 2 devices

40" x 1 screen

Relative size

Max dimensions
(w × h)

40" × 26"

21" × 28"

40" × 26"

Multi-touch capabilities

11 touch points
(per device)

2 touch points

2 to 32 touch points
(varies by model)

Apps supported

Web sites + iPad apps
(max 1 app per device)

Web sites + Android apps

  

Web sites + some Android apps + some Windows apps

Restricted web site browsing

Yes

Yes

Yes

Lock portions of applications

No

Yes

Yes

Trailer-capable

Limited
(backgrounds only)

Limited
(backgrounds and logo)

Yes

Usage data collection

No

No

Yes

Remote admin

Limited (iTunes)

Yes

Yes

Risks

1.    Lead time for vendor to frame iPads

 

1.    Hardware not shipping until April (22" version available now)

1.    Multi-touch not currently available for Android apps

2.    Requires unproven custom software

Hardware

Touchscreens

$5,300
($3,500 with 10 iPad Minis)

$600

$2,500

PC

$650

$0

$650

Software & labor 

Kiosk software

$100

$60

75 hours
(custom development)

Configuration & testing

20 hours

20 hours

20 hours

Remote management

50 hours

20 hours

0 hours

Metric collection & reporting

40 hours
(requires “jailbreaking” iPads)

20 hours

7 hours

 

Can you think of better configurations?  Please let me know!

Tags:

Hardware | Windows | Android | iOS

ViewSonic's 24" Android tablet

by timvasil 3/19/2013 5:53:00 PM

Now here's a device you're not going to misplace:  the 24" Android tablet from ViewSonic (VSD220).  It hasn't hit the market yet, but its underpowered 22" sibling has, and the reviews go something like this:  "Cool, but sluggish, and generally poor/blurry web browsing experience."

I called ViewSonic to get the scoop on whether the device supported a 90 rotation so I could run it in portrait mode.  After some checking, the support guy informed me that, sadly, no, rotation is not supported.  I'm surprised there's nothing else like it on the market right now:  this sort of device seems idea for kiosks and exhibits.

Tags:

Hardware

jQuery Sparklines + ExtJS 4

by timvasil 3/18/2013 1:11:00 AM

I wrote an Ext JS column subclass to render jQuery Sparklines with Sencha Ext JS 4 within an Ext JS grid. 

The code is on GitHub: https://github.com/timvasil/Sparkline

The demo is on jsFiddle: http://jsfiddle.net/timvasil/2gVUh/1/

Features include:

  • Compound sparklines
  • Single sparkline config for entire column
  • Separate sparkline configs for each cell in a column
  • Asynchronous sparkline rendering  (in configurable increments) to minimize lag
  • Optional override to minimize flicker on updates
  • Automatic sparkline resizing when columns are resized (for relative-width sparklines, e.g. "50%")

Tags:

JavaScript | jQuery | ExtJS

Seeing stars: simple star rating CSS

by timvasil 3/16/2013 8:52:00 AM

Taking a cue from Twitter Bootstrap icons, I cooked up the following CSS to drop in a 5-point star rating label with quarter point increments using a single HTML tag and no JavaScript. 

# starsMarkupPreview
0 stars  <i class="star"></i>  
0.25 stars  <i class="star star-qtr"></i>  
0.5 stars  <i class="star star-half"></i>  
0.75 stars  <i class="star star-3qr"></i>  
1 star  <i class="star star-1"></i>  
2 stars  <i class="star star-2"></i>  
3 stars  <i class="star star-3"></i>  
4 stars  <i class="star star-4"></i>  
4.5 stars  <i class="star star-4 star-half"></i>  
4.75 stars  <i class="star star-4 star-3qtr"></i>  
5 stars  <i class="star star-5"></i>  

This is the CSS:

i.star {
	background-image: url(../img/star-sprite.png);
	display: inline-block;
	height: 16px;
	width: 80px;
	background-position-x: -80px;
	background-position-y: -48px;
}

i.star-1    { background-position-x: -64px; }
i.star-2    { background-position-x: -48px; }
i.star-3    { background-position-x: -32px; }
i.star-4    { background-position-x: -16px; }
i.star-5    { background-position-x: 0; }
i.star-qtr  { background-position-y: -32px; }
i.star-half { background-position-y: -16px; }
i.star-3qtr { background-position-y: 0; }

Here's the image spite that goes along with it, in the spirit of the Amazon star sprite:

As I was working on implementing interactivity with these stars, so users could offer their own ratings, I realized there were a number of jQuery plugins out there that did something similar—even providing a read-only mode to accomplish something similar to what I did, including half-, quarter-, and even third-star ratings. RateIt seems to be one of the most robust ones to date.

The stars that ship with that plugin seem a rough around the edges—lacking semi-transparency for a smooth appearance—and were a bit too large, so I created a png alternative:

Before:After:

Tags:

CSS

Search

Calendar

«  December 2014  »
SuMoTuWeThFrSa
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar

Recent comments

Archive