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 192013
 

I just released version 1.2.1 of VirtualScroller. This minor update adds two enhancements:

  • Any number of items (at least 1) is supported. If less than 6 items are specified, the VirtualScroller falls back on a standard ScrollableView with the fancy scroll logic disabled. This is transparent to developers and users. Resolves issue 3.
  • VirtualScroller automatically detects whether you want finite or infinite scrolling. If the itemCount property is omitted, infinite scrolling is assumed. Otherwise finite scrolling is used. As such, the infinite property is no longer used. Resolves issue 4.

Download the latest version
Visit the Wiki

Enjoy!

 Posted by at 8:51 pm
Aug 172013
 

It’s been almost a year (exactly a year in seven days), but I finally have a new version of VirtualScroller! Version 1.2 is versioned as a minor update (in the 1.x family), but it contains some significant bug fixes and stability improvements. With the exception of a change in default options (see below), this is a drop-in replacement for version 1.1.1 and 1.1.

Glitch free, smooth scrolling

Previously, scrolling through a VirtualScroller too fast could leave the control in a transient state. Before version 1.2, I had worked around this issue by essentially limiting the scrolling speed. This resulted in an annoying user experience, because it was impossible to quickly swipe through pages. In fact, it was only possible to scroll through one page at a time, with a short pause in between pages.

This is no longer the case with 1.2. Users should not notice any jittering as they swipe along, thanks to vastly improved scrolling logic and event handling.

Two important internal changes made this possible:

  1. Increased view cache: Previously, only three views were maintained in memory which meant that the active view was padded on both sides by only a single view. Because of this, it was possible to scroll to the end of the in-memory views before the VirtualScroller had loaded the next views. Version 1.2 works around this problem by using a cache size of five. This makes it harder to outpace the VirtualScroller’s caching. Note: This means that your VirtualScroller itemCount MUST be at least five (or infinite), or else the VirtualScroller constructor will return null.
  2. Simplified scrolling logic: The code, in general, has been heavily refactored and simplified. For example, VirtualScroller now has a much easier check to determine if a scrollEnd event actually resulted in a page advance. As a consequence, there is much less that can go wrong.

Touch support is assumed

A minor change: previously, the touch option defaulted to false. Since all Google Play apps require touch support, I realize it is more convenient to default it to true instead. In version 1.2, touch now defaults to true.

Updated documentation

The (previously neglected) Wiki has been updated to reflect the new changes. Also, the code example should actually work now.

Download and documentation

Download the latest release here, and get the documentation here. As usual, if you encounter any issues or have any suggestions, please report them here.

Future development

I’m in the process of adding methods for advancing the control forwards and backwards, but I don’t have estimates for when that will be done.

Enjoy!

 Posted by at 9:11 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
Aug 242012
 

I’ve updated VirtualScroller (grab the latest download here). Here is the change log for the new version:

  • Breaking change: The return value of the factory method now returns an object, not a View. To access the VirtualScroller view (to add it to your view or window), you need the view property of the return value. See the Wiki for more information.
  • Dispose method: Call the dispose method to clean up event listeners and views used by the VirtualScroller instance. Not required but highly suggested.
  • Improved anti-jitter: With this new update I can almost guarantee that your VirtualScroller instance will never end up in an invalid state. In the past, this has happened when someone scrolls the VirtualScroller too fast.
  • Improved options handling: Internally, I’ve added deep option defaulting, which gives you the ability to override properties in sub-options (like top in containerDef, for example) without overriding the entire object. This functionality is similar to jQuery’s $.extend.
  • Accessibility: Set touchSupported to false (the default is true) to enable support for trackball navigation of the VirtualScroller. This is useful if you plan to publish your app on Amazon Appstore, or another app store that doesn’t require devices to have touchscreens.
  • Auto-focus: Set autoFocus to true and the first child of each view will be focused automatically upon viewing.

Access the updated documentation here.

 Posted by at 4:04 pm
Aug 152012
 

I looked through the Titanium documentation and user boards, but I couldn’t find a simple way to detect whether a device has a touch screen (as opposed to a trackball). Obviously, all iDevices will have touch screens, so this code is really meant for Android and web deployments.

Here is my very primitive (but functional) solution. Place this code where the first window that your user interacts with is created.

    Titanium.App.Properties.setBool("touch", false);

    function touchStart(){
        Titanium.App.Properties.setBool("touch", true);
        self.removeEventListener("touchstart", touchStart);
    }

    self.addEventListener("touchstart", touchStart);

The first time the user touches the screen (indicating that a touch screen is present), an application-wide setting is written indicating touch support.

In the rest of your application, check for touch support with:

var hasTouchSupport = Titanium.App.Properties.setBool("touch", false);

Note: I found that it was necessary to first clear the setting, for some reason, at least during debugging on an emulator. You could probably get rid of that first line.

 Posted by at 12:28 pm
Aug 032012
 

If anyone else is having trouble with the combo of Titanium, the Android SDK, and Windows 7 64-bit, then maybe this post if for you. It took me a few hours to get right.

Upon clicking “Android Emulator” under the Run button, I was receiving errors about jarsigner and java missing. Here’s what you have to do:

  1. Titanium requires the 32-bit version of the Java SDK, even if you are running on 64-bit Windows. Furthermore, it requires Java 6; Java 7 is the default.
  2. Uninstall all other versions of Java, and download Java 6 (which appears to be version 1.6, as opposed to Java 7 which appears as 1.7) for 32-bit from here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
  3. Install that.
  4. Set your JAVA_HOME path by following these steps: Go to your Start Menu, right click on Computer, click Properties. Click Advanced System Settings on the left, go to the Advanced tab, and click the Environment Variables… button at the bottom. Under System Variables, and click New… Name is JAVA_HOME, and value is something like C:Program Files (x86)Javajdk1.6.0_33, with the path possibly varying depending on the exact version of Java you installed.
  5. Now, add the bin directory to your PATH variable. Still in Environment Variables, under the System Variables section, find Path. Click Edit…, and in the Value box, append a semicolon if one isn’t already there. Then, append C:Program Files (x86)Javajdk1.6.0_33bin, taking note that your exact path might be a little different.
  6. Click Ok twice to get out of both dialogs.
  7. Restart your computer – yes, really. It simply won’t work if you don’t.

Side-rant: Why can’t Java configure the damn path variables for me? It couldn’t possibly be that hard to have the installer perform these two simple steps. I propose a new slogan. Java™ : Complexity for the sake of tradition.

Further troubleshooting

If you still get errors, then make sure you got all the paths to your JDK right. Actually navigate to the exact directory and copy the path from the address bar, instead of relying on the exact paths that I posted above.

If you still get errors, then make sure you have the right version of the JDK installed. Remember: 32-bit, Java 6 (1.6).

 Posted by at 9:40 pm
Jul 272012
 

Time to release something new. I’ve been working on a little project to develop an Android app. I’ve chosen Titanium because the app is simple (no network connectivity, sensor interaction, maps, etc.) and the learning curve for Titanium is low.

I had been working with the Android SDK for about a week (very frustrating), and one feature I found to be missing from Titanium is a decent way to scroll through large datasets horizontally. In my case, I had a few dozen pages that needed to be scrolled. I didn’t want to plop all of them into a ScrollableView, since that would put a strain on Android. So, I created this little module.

It is based off of this GitHub gist, though it doesn’t bear much resemblance anymore. Scroll down for links to my project.

VirtualScroller

The module I created is called VirtualScroller, and you use it like this:

// Replace the path as necessary
var VirtualScroller = require('ui/common/VirtualScroller');

var virtualScroller = VirtualScroller({
    itemCount: 10,
    getView: function(i) {
        return Titanium.UI.createLabel({
            width: Titanium.UI.FILL,
            height: Titanium.UI.FILL,
            text: "This is item " + (i + 1)
        });
    },
    infinite: false
});

window.add(virtualScroller);

I’ll steal the description from my BitBucket page, since I don’t feel like writing a new one:

VirtualScroller is an easy to use Titanium module that wraps around a ScrollableView and provides finite and infinite scrolling. All items (which are actually Views) are created on-demand, and only remain in memory when needed. The ScrollableView instance has three Views, which act as containers for the actual items.

This method permits memory-efficient scrolling of large sets of views.

So, if you have a large amount of views to scroll through, but don’t want to load them into memory until they’re needed, check out my module. It supports finite and infinite scrolling, and uses a callback to generate each view only when needed.

On BitBucket:
Wiki (instructions and information)
Downloads

 Posted by at 12:24 pm
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