Showing posts with label JavaScript. Show all posts
Showing posts with label JavaScript. Show all posts

Friday, November 14, 2014

Refresh/Update jQuery Selector after Ajax or other DOM Manipulation

jQuery Logo

I'm working on a more or less single page app right now. As such, there's a lot of dynamic DOM manipulation. Sometimes, we need to re-evaluate a jQuery selector to get the new set of objects after the DOM has changed.

Typically we've solved this problem by always using the selector to find the children. This became really frustrating in my Jasmine tests where I found myself doing this a lot!

    it("toggles the display of an element", function() {
        var $togglableItem = $('.togglable');
        expect($togglableItem.length).toBe(0);
        
        $button.trigger('click');  
        var $togglableItem = $('.togglable');
        expect($togglableItem.length).not.toBe(0);
        
        $button.trigger('click');  
        var $togglableItem = $('.togglable');
        expect($togglableItem.length).toBe(0);
    });

Even in production code (outside of tests) that can be kind of a pain. I wanted to be able to do something more like this:

    it("toggles the display of an element", function() {
        var $togglableItem = $('.togglable');
        expect($togglableItem.length).toBe(0);
        
        $button.trigger('click');  
        expect($togglableItem.refresh().length).not.toBe(0);
        
        $button.trigger('click');  
        expect($togglableItem.refresh().length).toBe(0);
    });

A Common Solution

I've seen some implementations of a jQuery refresh plugin that look like this:

(function($) {
    $.fn.extend({
        refresh: function() { return $(this.selector); }
    });
})(jQuery);

I have two problems with this approach. First, I don't always have my elements attached to the DOM when I want to do a refresh. In fact, typically in testing, I don't add my test elements to the DOM so I can't just reuse the selector. The selection depends on the parent jQuery object.

Second, I also really enjoy the fluency of jQuery and often make use of .end() method when it makes sense. The approach above flattens the selector stack and .end() returns the default jQuery object.

Preferred Approach

To maintain the fluency of jQuery and allow the refresh, here's what I'm proposing:

(function($) {
    $.fn.extend({
        refresh: function() { 
            var $parent = this.end();
            var selector = this.selector.substring($parent.selector.length).trim();
            return $parent.find(selector); 
        }
    });
})(jQuery);

Caveats

I haven't used this in the wild yet. I know .selector is deprecated, but it's the best I could find for now. It does, however, work pretty darned well in my Jasmine tests. :)

Saturday, March 15, 2014

A Limerick Standup: AngularJS, PhantomJS, and Jasmine

Logs

In honor of St. Patty's Day, I decided a limerick standup would be appropriate:

I've made some good progress with Angular
Though nothing of mention particular
     I have some good tests
     That pass in PhantomJS
And if Jasmine were here, I would strangle 'er

Friday, February 10, 2012

JavaScript for Photoshop - When Macros Aren't Enough

This2That Tile Sample

A few years ago, a fellow named Brian Dorn got in touch with me. He was working on his doctoral dissertation at Georgia Tech and he needed participants. I met him in Atlanta and found out that his study was about scripting with Photoshop. That was the first I'd ever heard of Adobe Photoshop Scripting.

A few weeks ago when I started working on a new word game This2That for Mobile Magic Developers, I needed to generate 38 tiles with letters, numbers, and punctuation. The task was tedious and the macros just couldn't make it any easier. Then I realized I had the embossing all wrong and I had to start over!

Frustrated, I tried to think of a better way. I remembered Brian Dorn and I started looking into this Adobe Photoshop JavaScript thing. I was very pleased when 15 minutes of script became an easily reusable tile generating utility. Now, I can take any PSD, open it up, select any text layer, and have the script generate a PNG for each letter I need

I thought this little known feature would make for interesting reading for both programmers and designers so here's my script:

// call the method that does all of the work
main();

// wrap the code in a method to make it easier to debug
function main() {

 // make sure you're working in a document and have a text layer selected
 if (!activeDocument || !activeDocument.activeLayer || activeDocument.activeLayer.kind != LayerKind.TEXT)
 {
   alert("Please select a document and a target text layer.");
   return;
 }
 
 // set up some information about the current file
 var textLayer = activeDocument.activeLayer;
 var path = activeDocument.path;
 var fileName = activeDocument.name;
 
 // remove the extension on the file name
 var extensionPosition;
 if (extensionPosition = fileName.lastIndexOf('.'))
  fileName = fileName.substr(0, extensionPosition);

 // get a good place to put the file
 var outputFolder = Folder.selectDialog("Select a target folder.", path);
 
 // set up the letters we want images for
 var characterMap = [
  ["question", "?"]
 ];
 
 // and add the lowercase alphabet and numbers
 characterMap = characterMap.concat(getAsciiRange(97, 26), getAsciiRange(48, 10));

 // for each character, update the selected text layer and save a file
 for (var i = 0; i < characterMap.length; i++)
 {
   var character = characterMap[i][1];
   var fileSuffix = characterMap[i][0];
   
   textLayer.textItem.contents = character;
   
   var file = new File(outputFolder + "/" + fileName + "_" + fileSuffix + ".png");
   var options = new PNGSaveOptions();
   options.interlaced = false;

   activeDocument.saveAs(file, options, true, Extension.LOWERCASE);
 }
};

// a little helper method to make a range of letters and their
// filename extensions
function getAsciiRange(from, count) {
 var result = [];

 for (var i = 0; i < count; i++)
 {
  var character = String.fromCharCode(i + from);
  result.push([character, character.toUpperCase()]);
 } 
 
 return result;
}
 

Friday, June 17, 2011

GiantDropdown jQuery Plugin for Styling Large Select Lists

A few months ago, I was working on a performance management application for the Centers for Disease Control. They wanted a select box that contained a list of competencies from which they could compose a template. The problem was some of the items in the list were hundreds of characters long and, at the time of writing, most browsers would behave very badly with drop down boxes of that size.

I was sitting at home that night and decided I could probably come up with a jQuery plugin that would use the select list as the backing field and present a prettier display. I did and we ended up using it at the CDC. I decided I'd share in case anyone else runs into the same issue.

Here's an example of what one of the select boxes would look like:


It's not very pretty and it behaves unpredictably with other DOM elements. With the GiantDropdown plugin, we can style it any way we choose and still preserve all of the original behavior of the select list (meaning, it even works with multi-selects and option groups).

Here are some samples of the giant-ified dropdowns:


If you'd like to get the giant dropdown jquery plugin, the can check out the Giant Dropdown jQuery Plugin Git Repository. Feel free to make improvements.

Tuesday, September 21, 2010

Array Function to Recode Data in Google Apps Scripts

Google Docs IconI went on my honeymoon with my beautiful wife last week and the week before. Having a little time off of work gave me the opportunity to get some work done :). I've been wanting for a while to develop a survey to find out what makes a good programmer.

I've been working with Google Docs and Google Forms to see what they're capable of. This spreadsheet posed a few difficulties. The primary problem was that I had a set of text values which needed to be recoded to numerical values from another range.

I wrote this array function for Google Apps Scripts in Google Spreadsheets to recode values based on an array of values.

Here's an example spreadsheet demonstrating the Array Data Block Recode Function.

Here's what the function looks like with a few tests:
function recode(data, values, valueColumnIndex)
{
  var  valueHash = {};
  
  // if the values are in an array, make a hash table
  if (values.constructor == Array)
    for (var i = 0; i < values.length; i++)
      valueHash[values[i][0]] = values[i][valueColumnIndex];

  else
    valueHash = values;
  
  var ret = [];
  
  // if the data are in an array, recursively recode them
  if (data.constructor == Array)
    for (var i = 0; i < data.length; i++)
      ret.push(recode(data[i], valueHash, valueColumnIndex));
          
  else
    ret = valueHash[data] != undefined ? valueHash[data] : data;

  return ret;
}

var values = [['a', '1', 'I'], ['b', '2', 'II']];

print(recode('a', values, 1));
print(recode(['a', 'b', 'c'], values, 1));
print(recode([['a', 'b'], ['b', 'c']], values, 1));
print(recode(['a', ['a', 'b'], [['a', 'b', 'c']]], values, 2));

/*
Results:
1
[1, 2, 'c']
[[1, 2], [2, 'c']]
['I', ['I', 'II'], [['I', 'II', 'c']]]
*/

/*
Google Apps Syntax:
=Recode(A1:B3, D1:F2, 1)
=Recode(A1:B3, D1:F2, 2)
*/

Regular Expression Search Bookmarklet

iPhone BookmarkletsI have an old website where I keep most of my bookmarklets. I'm planning on deprecating that site and just putting up some personal stuff (since I don't do what that site says I do anymore).

This bookmarklet is probably my most used bookmarklet. Basically, you enter a regular expression and each match in the page will be highlighted. It cycles through 16 color schemes to change the highlight color.

If you just want to install the bookmarklet, grab this link and drag it onto your bookmarklet toolbar in your browser.
Regex Search

If you want to use it on your mobile device, you can use my Mobile Bookmarklet Installer Bookmarklet (which, by the way, will install itself too).

I'm sure it doesn't work in IE, but I haven't tested it in a really long time. If you'd like to see what it would do in IE if IE didn't suck so badly, just click it.

If you're interested in the code, here it is!
// check to see if the variable searches has been defined.
// if not, create it.  this variable is to cycle through 
// highlight colors.
if (typeof(searches) == 'undefined')
{
  var searches = 0;
};

(
  function()
  {
    // just some variables
    var count = 0, text, regexp;

    // prompt for the regex to search for
    text = prompt('Search regexp:', '');

    // if no text entered, exit bookmarklet
    if (text == null || text.length == 0)
      return;

    // try to create the regex object.  if it fails
    // just exit the bookmarklet and explain why.
    try
    {
      regexp = new RegExp(text, 'i');
    }

    catch (er)
    {
      alert('Unable to create regular expression using text \'' + text + '\'.\n\n' + er);
      return;
    }

    // this is the function that does the searching.
    function searchWithinNode(node, re)
    {
      // more variables
      var pos, skip, acronym, middlebit, endbit, middleclone;
      skip = 0;

      // be sure the target node is a text node
      if (node.nodeType == 3)
      {
        // find the position of the first match
        pos = node.data.search(re);

        // if there's a match . . . 
        if (pos >= 0)
        {
          // create the acronym node.
          acronym = document.createElement('ACRONYM');
          acronym.title = 'Search ' + (searches + 1) + ': ' + re.toString();
          acronym.style.backgroundColor = backColor;
          acronym.style.borderTop = '1px solid ' + borderColor;
          acronym.style.borderBottom = '1px solid ' + borderColor;
          acronym.style.fontWeight = 'bold';
          acronym.style.color = borderColor;
    
    // get the last half of the node and cut the match
    // out.  then, clone the middle part and replace it with
    // the acronym
          middlebit = node.splitText(pos);
          endbit = middlebit.splitText(RegExp.lastMatch.length);
          middleclone = middlebit.cloneNode(true);
          acronym.appendChild(middleclone);
          middlebit.parentNode.replaceChild(acronym, middlebit);
          count++;
          skip = 1;
        }
      }

      // if the node is not a text node and is not
      // a script or a style tag then search the children
      else if (
        node.nodeType == 1
        && node.childNodes
        && node.tagName.toUpperCase() != 'SCRIPT'
        && node.tagName.toUpperCase != 'STYLE'
      )
        for (var child = 0; child < node.childNodes.length; ++child)
          child = child + searchWithinNode(node.childNodes[child], re);

      return skip;
    }

    // use the search count to get the colors.
    var borderColor = '#' 
      + (searches + 8).toString(2).substr(-3)
      .replace(/0/g, '3')
      .replace(/1/g, '6');
    
    var backColor = borderColor
      .replace(/3/g, 'c')
      .replace(/6/g, 'f');

    // for the last half of every 16 searhes, invert the
    // colors.  this just adds more variation between
    // searches.
    if (searches % 16 / 8 >= 1)
    {
      var tempColor = borderColor;
      borderColor = backColor;
      backColor = tempColor;
    }

    searchWithinNode(document.body, regexp);
    window.status = 'Found ' + count + ' match'
      + (count == 1 ? '' : 'es')
      + ' for ' + regexp + '.';

    // if we made any matches, increment the search count
    if (count > 0)
      searches++;
  }
)();

Thursday, March 11, 2010

Custom jQuery Selector for External and Internal Links

jQuery LogoI was working on a website for my fiancee and her church group called The Diocese of Atlanta Young Adults. They were hosting an event they call the Young Adult Summit.

One of the requirements I had was to provide a warning to users before they left the page by after having clicked an external link. Using jQuery, I was able to bind to the click event with easy cross browser compatibility and I used jQueryUI to open a modal dialog box. Pretty basic stuff.

The one thing I regretted was that I had to use a class name to determine which links were external and which were internal. A few days ago, I discovered that jQuery supports custom selectors and I decided to write a pair of custom selectors for identifying internal and external links.
jQuery.extend(
  jQuery.expr[ ":" ],
  {
    /*
      /:\/\// is simply looking for a protocol definition.
      technically it would be better to check the domain
      name of the link, but i always use relative links
      for internal links.
    */

    external: function(obj, index, meta, stack)
    {
      return /:\/\//.test($(obj).attr("href"));
    },

    internal: function(obj, index, meta, stack)
    {
      return !/:\/\//.test($(obj).attr("href"));
    }
  }
);
I do have a few items of note. First, you'll notice that I'm not actually looking for the current domain name in the links. That's because I use relative links for all of my internal links these days. Thus, I know that if there's not a protocol definition that it's an internal link. If you need to identify internal links by domain as well, you can just pull that out of top.location.href and check for it too.

Second, you could technically use this selector on any DOM object. There are several ways to get around this. One way would be to verify that the object is an anchor tag. Another would be to verify that the href attribute exists. I just plan on using common sense.

Here's a quick usage example:
$("a:external").click(verifyNavigateAway);
$("a:internal").css("font-weight", "bold");