Apr 112015
 

Some motivation

Django has a field called ChoiceField which lets you very easily create a <select> dropdown in your forms. A great feature is the ability to transform nested choices into appropriate <optgroup> groups. For instance, the following code:

my_choices = [("Group 1", [
                 (1, "Choice 1"), (2, "Choice 2")]), 
              ("Group 2", 
                 [(3, "Choice 3"), (4, "Choice 4")]), 
              (5, "Choice 5")]
field = ChoiceField(label="Field", choices=my_choices)

…creates a field that when rendered looks something like this:

drop

Looks nice

 

The HTML that Django produced looks like this (excluding the label):

<select id="id_test" name="test">
    <optgroup label="Group 1">
        <option value="1">Choice 1</option>
        <option value="2">Choice 2</option>
    </optgroup>
    <optgroup label="Group 2">
        <option value="3">Choice 3</option>
        <option value="4">Choice 4</option>
    </optgroup>
    <option value="5">Choice 5</option>
</select>

Nice. So, what happens when we try to add more levels of nesting in the choices?

my_choices = [
    ("Group 1", [
        (0, "Choice 0"),
        ("Subgroup 1", [
            (1, "Choice 1")]),
        ("Subgroup 2", [
            (2, "Choice 2")])]),
    ("Group 2",[
        ("Subgroup 3", [
            (3, "Choice 3")]),
        ("Subgroup 4", [(4, "Choice 4")])]),
     (5, "Choice 5")]
drop2

Bad, bad, bad

 

That didn’t work. Here’s the HTML Django gave us:

<select id="id_test" name="test">
    <optgroup label="Group 1">
        <option value="0">Choice 0</option>
        <option value="Subgroup 1">[(1, &#39;Choice 1&#39;)]</option>
        <option value="Subgroup 2">[(2, &#39;Choice 2&#39;)]</option>
    </optgroup>
    <optgroup label="Group 2">
        <option value="Subgroup 3">[(3, &#39;Choice 3&#39;)]</option>
        <option value="Subgroup 4">[(4, &#39;Choice 4&#39;)]</option>
    </optgroup>
    <option value="5">Choice 5</option>
</select>

 

The problem

As it turns out, ChoiceField only supports one level of nesting. Five years ago someone submitted a patch to correct the problem, but it was rejected for good reason: HTML itself only officially supports one level of <optgroup> nesting.

To see if it would work anyway, I manually applied the patch to my Django installation. Although the patch itself worked (Django produced the nested option groups), Chrome wasn’t having any of it. My dropdown displayed incorrectly, and developer tools showed that the HTML got mangled during parsing.
 

Emulating nested optgroups

My solution to the problem is to fake it by using disabled <option> elements as group headers. Then, we apply indentation to the actual options to make them appear to belong to the groups. This is accomplished by a custom widget called NestedSelect (below), which is a cross between the original Select widget and the patch I linked to above.

from itertools import chain
from django.forms.widgets import Widget
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.forms.utils import flatatt

class NestedSelect(Widget):
    allow_multiple_selected = False

    def __init__(self, attrs=None, choices=()):
        super(NestedSelect, self).__init__(attrs)
        # choices can be any iterable, but we may need to render this widget
        # multiple times. Thus, collapse it into a list so it can be consumed
        # more than once.
        self.choices = list(choices)

    def render(self, name, value, attrs=None, choices=()):
        final_attrs = self.build_attrs(attrs, name=name)
        output = [format_html('<select{}>', flatatt(final_attrs))]
        selected_choices = set(force_text(v) for v in [value])
        options = 'n'.join(self.process_list(selected_choices, chain(self.choices, choices)))
        if options:
            output.append(options)
        output.append('</select>')
        return mark_safe('n'.join(output))

    def render_group(self, selected_choices, option_value, option_label, level=0):
        padding = "&nbsp;" * (level * 4)
        output = format_html("<option disabled>%s{}</option>" % padding, force_text(option_value))
        output += "".join(self.process_list(selected_choices, option_label, level + 1))
        return output

    def process_list(self, selected_choices, l, level=0):
        output = []
        for option_value, option_label in l:
            if isinstance(option_label, (list, tuple)):
                output.append(self.render_group(selected_choices, option_value, option_label, level))
            else:
                output.append(self.render_option(selected_choices, option_value, option_label, level))
        return output

    def render_option(self, selected_choices, option_value, option_label, level):
        padding = "&nbsp;" * (level * 4)
        if option_value is None:
            option_value = ''
        option_value = force_text(option_value)
        if option_value in selected_choices:
            selected_html = mark_safe(' selected="selected"')
            if not self.allow_multiple_selected:
                # Only allow for a single selection.
                selected_choices.remove(option_value)
        else:
            selected_html = ''
        return format_html('<option value="{}"{}>%s{}</option>' % padding, option_value, selected_html,
                           force_text(option_label))

Then it’s just a matter of swapping out the widget that ChoiceField uses:

self.fields["test"] = ChoiceField(label="Field", choices=my_choices, 
     widget=NestedSelect)

Now we get something closer to what we wanted:

drop3
 

Improving the aesthetics

The solution above works, but it can be improved in two ways:

  1. The manual addition of padding results in an ugly side-effect – that padding is visible in the selected option. Look at the difference when Choice 1 and Choice 5 are selected (below). Choice 5 is aligned correctly, but Choice 1 appears to be floating out in no-man’s land.

1 2

  1. We no longer have the nice bolding effect on the option group headers. It’s not possible for us to individually style the elements that are supposed to be headers.

To overcome these shortcomings of HTML, we must turn a JavaScript based replacement: Select2. With that plugin installed, the fix is just a little bit of JavaScript:

$(function () {
    $("select").each(function(){
        // Ensure the final select box is wide enough to fit the bolded headings
        $(this).width($(this).width() * 1.1);
    }).select2({
        templateSelection: function (selection) {
            return $.trim(selection.text);
        },
        templateResult: function (option) {
            if ($(option.element).attr("disabled")) {
                return $("<b>" + option.text + "</b>").css("color", "black");
            } else {
                return option.text;
            }
        }
    });
});

 

Finished product

The final results look nice, and you even get a free search box thanks to Select2:

222

 Posted by at 5:03 pm
Oct 132012
 

jQuery UI 1.9 is finally here! What does that mean for dialogWrapper? Well, it would seem like the answer to that question is: nothing. A redesign of the dialog API is not present in version 1.9, and it doesn’t look like it will happen until the 2.0 release, according to the official site.

The planned changes for the 2.0 release are something to get excited about now, however. Some of the changes discussed add some nice functionality and improve the overall design of the API. Heads up: so far, it doesn’t sound like the 2.0 release will make dialogWrapper irrelevant.

 Posted by at 11:57 am
Jun 032012
 

Changes, changes. I’ve just finished adding the grunt build system to all my JavaScript projects. This has made it so much easier to manage them.

Now instead of manually generating the minified versions (copy-pasting to Google Closure Compiler), worrying about the license files being up to date, etc., I can issue a single command and everything is taken care of.

This also means that JSLint is run whenever I build my projects. So naturally, in the span of a few hours I caught a bunch of warnings and errors that I had previously never seen. I’ll go through the projects one at a time.

 

Textarea Line Count

Very important fixes. Updated to version 1.4. Let me say, I’m surprised that I didn’t receive any bug reports on version 1.3. Running JSLint revealed variables that didn’t exist, or that were named incorrectly. Oops. That’s what I get for not using a build system sooner.

 

dialogWrapper

Updated to version 2.1.1. No bug fixes here, just a few minor changes to keep JSLint happy.

 

wrapDetector

Updated to version 1.0.1. Same as with dialogWrapper.

 Posted by at 8:13 pm
Dec 142011
 

IMPORTANT: Please read this blog post before updating. The latest version, by default, breaks compatibility with previous versions!


I am happy to announce that dialogWrapper 2.1 has been released! Download/read about it at BitBucket: https://bitbucket.org/MostThingsWeb/dialogwrapper

 

What’s new?

The biggest change is that all methods are now namespaced! For example, instead of writing:

$.alert("This is an alert dialog!");

… by default you must now write:

$.dW.alert("This is an alert dialog!");

Why did I do this? Well, I was getting concerned about my polluting the global $ namespace. It should now be easier for my plugin to work with other plugins that may have used the same or similar function names.

 

But that breaks compatibility!?

You can use the new $.dW.classicMode() method to restore the old style functions. Just call it once, with no arguments. Then you can go back to using $.createDialog and friends instead of the new namespaced versions.

 

What else is new?

This version has a few new nice features:

  • The callbacks for $.dW.alert(), $.dW.confirm(), and $.dW.input() are now executed within the context of the dialog. That means that you can now use this within the handler to reference the dialog.
  • I added a callback to $.dW.alert() that is executed when the Ok button is clicked.
  • Options that dialogWrapper adds, with hasClose being the only one so far, are now defaulted in $.ui.dialog.prototype.options, along with native options. This way, you can easily modify the default hasClose setting.
  • You can now change the hasClose option at runtime using the usual method.
  • From this version forward, you can access the version string using $.dW.version.
  • I’ve exposed $.dW.findDialog, which is a utility method that attempts to resolve the given identifier to a dialog object.

 

And a couple of minor changes:
  • I’ve removed the smartModalsForClassicDialogs and smartModals options since they became redundant.
  • The default prefix for dialog IDs is now dWd instead of dwd.
  • A few bug fixes.
  • A couple of internal changes; if you are interested, read the full changelog in the source.
 Posted by at 4:06 pm
Dec 102011
 

I really enjoy exploring jQuery’s source code. Recently, I’ve been using James Padolsey’s jQuery Source Viewer. It’s a really neat tool that lets you type in the name of a function (even if it’s an internal function!) and view its source.

Just today, I’ve discovered a different tool/series called JS Libs Deconstructed. Not only will it let you explore jQuery’s source code, but it also works with Prototype and MooTools. But, since I’m more interested in jQuery, I’ll just link the jQuery version: http://www.keyframesandcode.com/resources/javascript/deconstructed/jquery/.

 

This blurb from their website sums up the purpose of the tool nicely:

The Deconstructed series is designed to visually and interactively deconstruct the internal code of JavaScript libraries, including jQueryPrototype and MooTools.

It breaks the physical JavaScript into visual blocks that you can easiliy navigate. Each block opens to reveal its internal code. Clickable hyperlinks allow you to follow program flow.

What is interesting about the tool is that you do not necessarily need to know the name of the function you are looking for.  What’s more, you can explore more than just functions! If, for example, you are interested in how jQuery can recognize attributes in selectors (e.g. $("div[name=mydiv]")), you can go to the Attributes section (1/3 of the way down), and view the Regular expressions that jQuery uses. In this particular example, the regular expressions are stored in a “private” variable, so it’s something that the other tool can’t access.

My only complaint is that individual sections and code blocks are not permalinked, but I’m sure that will happen in another update. Anyway, I can see it being very useful. Check it out: http://www.keyframesandcode.com/resources/javascript/deconstructed/.

 Posted by at 1:53 pm
Jul 062010
 

In my Greasemonkey scripts, I sometimes find it necessary to alert the user in some way without using the alert() function. In any other web application, I would normally use Facebox, a wonderfully free jQuery plugin that displays Facebook style pop-ups. However, there are several obstacles to adding any (well, most) jQuery plugins to Greasemonkey scripts:

  • The plugin itself is a script which must be kept somewhere. You can directly download the script from the Facebox server with Ajax, but that might result in an obscene amount requests to the server (which is not proper internet etiquette).
  • Some plugins require external media (i.e. images, CSS sheets, etc.), which, as described above, shouldn’t be downloaded directly from the owning server.
  • Media can sometimes be embedded in the script, but plugins can sometimes be too big or awkward to store inline.

Luckily, Greasemonkey and JavaScript contains the features necessary to resolve these issues.

The first and third issues can be solved with Greasemonkey’s GM_getValue() and GM_setValue() functions and a bit of Ajax. The idea is to download the plugin script once, and cache it using GM_setValue(). Then, the next time the script runs we can use GM_getValue() to get it.

The second issue can be solved using Firefox’s handy data URI feature. I used this website to upload the plugin’s image files and convert them to a data URI. In the code that injects Facebox, the references to these files in the plugin code is replaced with the file data, inline. Note that the Facebox CSS was minified as well, although I have misplaced the link to the website I used. Here is the code (or, alternatively, download it from here):

 // Facebox: Copyright 2007, 2008 Chris Wanstrath [ [email protected] ]

var loadingImg = &quot;data:image/gif;base64,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&quot;;

var closeImg = &quot;data:image/gif;base64,R0lGODlhQgAWAOYAAMDAwMHBwZ2dnVlZWdLS0oODg6+vr/X19eTk5FBQULi4uIyMjJSUlFRUVFZWVtzc3FJSUsnJyVFRUVhYWFVVVezs7FNTU/b29v39/erq6vr6+qampldXV6KiokxMTE9PT2ZmZvf3993d3czMzGBgYHt7e6Ghofz8/PDw8N7e3mxsbLW1tfv7+5WVlc3NzcrKynh4eOvr60tLS+Dg4F9fX7m5ud/f37S0tIaGhnBwcGNjY+Xl5ZKSkoiIiKmpqW5ubpeXl29vb9XV1Y6Oju3t7WVlZXZ2dpiYmG1tbXx8fO/v7+fn57GxsbOzs/Hx8Xl5eVpaWmRkZE1NTXNzc2dnZ5aWloeHh+Pj405OTmlpadvb23p6ev////7+/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAABCABYAAAf/gFyCg4SFhoeIiYqLjI2Oj4xdkpOUlZaXl5BcJ42Ynp+YkZJcLEdCXJZcIwIaqKCvoKJdIUAeUSKuXYIuNB5VF5QVWgiwXQjDlwdaWrqLo00yAxZFWq6qJA0DHj6SBAtb4AsEXQBbzJUABeBbBQCUCusFL1wB6+uDoxlbHwMJVDaCRtCwMAALDhRdImxZEEFLhAIGyJmrZGALAwJaCDDYElGigYwbLtDjGKBkAHz5SiQY0EDFDBfYBkixEgJVgQIHJuWUeE4Sgi0CKgmYOJSSoHoPDhmtUIIfBSTYJiToIbKLli3uLJXrybPSVQVdKirYeXRLUkOVuMRIAmGAAw4D/yAMqSlp6yW7k4paAtqlgrotGxCUbSGgsAKURlEYoTBgQoMOrSbhRTcx75ZLfLscAPDN7EjChhFP4qIkCGOWDHRNIoD1bmVJ8MZRYp1VMseRZwulnTElGwUHcU1gcHXg5k5jOSdbzWyZmOZJQHErHZ1ChQRpWUA4mAChw3BJChk6LLBgM0cA6JlVvJhxI9iw7DdsOVnPgMkARARRB3E9AYwrLziwnQTCueJNPOOUY09W6ayzQG0NgnPYSPZ4Zo0IIEAARQIlZCBIDQJOIEEHJ1ByjHOvnHjJMjlFwsUDOqzkXwyoCLKCBBxwIMMNxfSYijNcOMEABB9s4aFRK1iQQDAOIvjo5CjO6MICD09UkAuUTPyww5VPwiILFxpUdQkGS3DZ5SuyNOOJfmf6mGaPgQAAOw==&quot;;

var faceCss = &quot;#facebox .b{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAQCAYAAADwMZRfAAAAG0lEQVQ4jWMICgraQClmGDVk1JBRQ0YNCdoAAHYawHDC5WlcAAAAAElFTkSuQmCC')}#facebox .tl{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAS0lEQVR42o3OoQ0AIAxEUZYi7NEluggewwy1dMNyBgIJCSe+uTxxKSKuRKQgRRV1ZGicIKOG/NVGa/jB9oPrkzNQWVhZ2FloLBwMnD51rC95s060AAAAAElFTkSuQmCC')}#facebox .tr{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAARElEQVR42o3KoREAIAwEMJbi2KNLdBE8pjPUlg3Lo8BwvIhLEZEAB4MOCi0zy23H+TCg/uNR2TjYuDU2Khs7G42NzsZYRf6sL6b2F1EAAAAASUVORK5CYII=')}#facebox .bl{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQ0lEQVQY02MICgpaD8QbCGEGILGMWIVTiFXYQqzCdGIVmhOl8P///yDF/cQqNCVKIZLifoIKkTSYQz3YAg06UDivBwBLtawvNrYbVAAAAABJRU5ErkJggg==')}#facebox .br{background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQ0lEQVQYlY3KoRUAIAhFUZbyuMdbgkXoFmaw6oaYyP5w2zXgCo6Jcasx1RhqdDVOJa6qMiWOX1ydOh5gAwkE4MDs0B5TPqwv1+d6zQAAAABJRU5ErkJggg==')}#facebox{position:absolute;top:0;left:0;z-index:100;text-align:left}#facebox .popup{position:relative}#facebox table{border-collapse:collapse}#facebox td{border-bottom:0;padding:0}#facebox .body{padding:10px;background:#fff;width:370px}#facebox .loading{text-align:center}#facebox .image{text-align:center}#facebox img{border:0;margin:0}#facebox .footer{border-top:1px solid #DDD;padding-top:5px;margin-top:10px;text-align:right}#facebox .tl,#facebox .tr,#facebox .bl,#facebox .br{height:10px;width:10px;overflow:hidden;padding:0}#facebox_overlay{position:fixed;top:0;left:0;height:100%;width:100%}.facebox_hide{z-index:-100}.facebox_overlayBG{background-color:#000;z-index:99}* html #facebox_overlay{position:absolute;height:expression(document.body.scrollHeight&amp;gt;document.body.offsetHeight ? document.body.scrollHeight:document.body.offsetHeight+'px')}&quot;;

GM_addStyle(faceCss);
GM_addStyle(&quot;#facebox {font-family: 'lucida grande',tahoma,verdana,arial,sans-serif}&quot;);

// Cache the facebox JS to avoid bombarding the hosting site
var cache = GM_getValue(&quot;facebox&quot;, null);
if (cache == null)
{
	console.log(&quot;Script not in cache... fetching remote version&quot;);
	GM_xmlhttpRequest({
		'method':'GET',
		'url':&quot;http://defunkt.github.com/facebox/facebox.js&quot;,
		'onload':function(d){
			GM_setValue(&quot;facebox&quot;, d.responseText);
			injectAndContinue(d.responseText);
		}
	});
}
else {
	console.log(&quot;Script already in cache... loading cached version&quot;);
	injectAndContinue(cache);
}

function injectAndContinue(script)
{
	script = script.replace(&quot;loadingImage : '/facebox/loading.gif'&quot;,&quot;loadingImage : '&quot; + loadingImg + &quot;'&quot;);
	script = script.replace(&quot;closeImage   : '/facebox/closelabel.gif'&quot;, &quot;closeImage : '&quot; + closeImg + &quot;'&quot;);

	// Inject script
	$(&quot;&lt;!--mce:0--&gt;&quot;).appendTo('head');

	main();
}

Also note that this script assumes that the website the script is included in already uses jQuery. If not, you might want to inject it yourself.

Hopefully in the near future I will roll out a tool which automates the process of making jQuery plugins (and other scripts) suitable for inclusion in Greasemonkey scripts.

 Posted by at 7:22 pm