Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

#3202 - Large speed increases to SelectBox and SelectFilter2 #222

Closed
wants to merge 9 commits into from

5 participants

Kyle MacFarlane Julien Phalip Danilo Bargen Tim Graham Simon Charette
Kyle MacFarlane

I have an admin page with two filter_horizontal widgets on it. One has almost 800 options and the other 11,500. As you can imagine it's pretty slow but also it sometimes copies the wrong selections if you have a lot of them, plus it crashes on page load in IE and Opera. The scripts in 1.4 make it even slower by using a slow jQuery selector to check if anything is selected when deciding what icons to use.

The tweaks to redisplay from #3202 will cut a second or so off of every action but the code still calls redisplay way too often which means doing anything takes 2-5 seconds. Ticket #9102 attempted to avoid redisplay but its method of hiding <option> tags doesn't work in Chrome.

I've come up with the following which will generally transfer options instantly on my 11,500 list. It starts to slow down if you're moving stuff from side to side in bulk but it's still faster than what's currently in Django. It works excellently in Chrome, Firefox, and Safari, but Opera and IE are still slow but no longer crash.

  1. Included the tweak from the current SelectBox.patch on ticket #3202 to speed up redisplay.

  2. Don't run redisplay on page load or moving options between columns. Instead just move the actual DOM node and this is super fast.

  3. However, when moving large numbers of options then keeping them in alphabetical order is often slower than redisplaying, so redisplay instead. I currently set it to redisplay on any movement of more than 100 options but that number is just a guess and involved little testing. The number really depends on the amount of options being moved, how many options are in the destination column, how fast the user's browser is, etc.

  4. Only redisplay filter results 250ms after you've stopped typing. This prevents the major lag on the first 1-3 letters when you try to filter something on medium to large lists.

  5. Store a separate key map for quicker access to the cache and less iterating.

  6. Check against selectedIndex to see if any options are selected instead of going through every option again with jQuery just to display icons.

  7. Every browser except Firefox will lock up for 2-3 minutes if you have too many selected="selected" (even in a plain HTML file). So while you can use this javascript to select 10,000 options in 2 seconds your tab might crash after the form submit.

  8. Bonus: fixed the keyboard shortcuts and enter no longer bubbles up and submits the form.

django/contrib/admin/static/admin/js/SelectBox.js
((29 lines not shown))
20 23
             }
21 24
         }
  25
+        box.appendChild(fragment.cloneNode(true)); 
1
Simon Charette Owner
charettes added a note July 20, 2012

Why are you cloning here? Can't you just append the fragment?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Kyle MacFarlane
  1. I made some more speed improvements and it can now do about 4000 single node moves before a full redisplay is faster. Plus redisplays are faster too.

  2. Increased the delay on the typing setTimeout because too often it fired early. I then made it so that hitting enter sets the delay to 0 and immediately filters.

  3. The add new option popup no longer calls redisplay.

  4. I made large option moves maintain your scroll position so that you aren't lost in the middle of nowhere afterwards.

I think there's potential for usability improvements that can be done in relation to tabindex etc but I'm not sure how people would want the form to actually behave.

Kyle MacFarlane

The cache ended up having race issues now that it was working quickly, but I realised that it wasn't even needed anymore anyway.

I also changed the keyboard functionality to something better I believe.

  1. When you tab in you will first land on the green plus icon if it's available. Hitting enter will cause the popup and you can add a new object using only the keyboard.

  2. The second tab will land in the filter field. In this field you can obviously filter and use enter to to bypass the typing delay. The arrow keys no longer do anything here.

  3. The third tab lands you in the left column where you can use the up and down arrows to select options. The wrapping from top to bottom and vice versa was removed because it interfered with the default browser abilities such as using shift to select multiple options. You can use home and end to get to the top and bottom anyway. Once you've selected some options you can move them over with either enter, space, or the left/right arrow keys. In Webkit, Ctrl + A works in here by default but other browsers can use shift + home and end anyway.

  4. The fourth tab will land you in the right column which behaves the same.

  5. The fifth tab will leave the widget and enter the next field.

Julien Phalip
Owner

Thanks a lot for your work. Do you think you could reproduce the same performance improvements, but also rewrite the widget with jQuery? If so, then I think it'd be a better approach to kill these two birds with one stone. See this ticket for reference: https://code.djangoproject.com/ticket/15220

Danilo Bargen

Fixing this issue would be hugely appreciated :) I'm currently working with a project that displays 13k items in a filter_horizontal widget and the page takes 20-30 seconds to load.

ndarville ndarville referenced this pull request in ndarville/pony-forum March 24, 2013
Open

includes/manage-users.html #107

Tim Graham
Owner

Closing this in light of @jphalip's comment above.

Tim Graham timgraham closed this July 26, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
294  django/contrib/admin/static/admin/js/SelectBox.js
... ...
@@ -1,111 +1,249 @@
1 1
 var SelectBox = {
2  
-    cache: new Object(),
  2
+    initialised: false,
  3
+    options: new Object(),
3 4
     init: function(id) {
4  
-        var box = document.getElementById(id);
5  
-        var node;
6  
-        SelectBox.cache[id] = new Array();
7  
-        var cache = SelectBox.cache[id];
  5
+        var box = document.getElementById(id + '_from'),
  6
+            node;
  7
+
  8
+        SelectBox.options[id] = new Array();
8 9
         for (var i = 0; (node = box.options[i]); i++) {
9  
-            cache.push({value: node.value, text: node.text, displayed: 1});
  10
+            node.order = i; // Record the initial order
  11
+            if (django.jQuery.browser.msie) node.text_copy = node.text;
  12
+            node.displayed = true;
  13
+            node.select_boxed = false;
  14
+            SelectBox.add_to_options(id, node);
10 15
         }
  16
+
  17
+        SelectBox.move(id);
  18
+        // This prevents a jump on focus if options have been moved out
  19
+        box.selectedIndex = -1;
  20
+
  21
+        SelectBox.register_onpopstate();
  22
+        SelectBox.initialised = true;
11 23
     },
12  
-    redisplay: function(id) {
13  
-        // Repopulate HTML select box from cache
14  
-        var box = document.getElementById(id);
15  
-        box.options.length = 0; // clear all options
16  
-        for (var i = 0, j = SelectBox.cache[id].length; i < j; i++) {
17  
-            var node = SelectBox.cache[id][i];
  24
+    redisplay: function(id, large, all, webkit_repair) {
  25
+        // Repopulate HTML select box from options
  26
+        var from_fragment = document.createDocumentFragment(),
  27
+            to_fragment = document.createDocumentFragment(),
  28
+            from_box = document.getElementById(id + '_from'),
  29
+            to_box = document.getElementById(id + '_to'),
  30
+            node, add_to;
  31
+
  32
+        // Setting innerHTML doubles the speed by making it unnecessary for the
  33
+        // browser to compliment appendChild with removeChild. For example, in
  34
+        // Chrome it literally doubles the speed of moving nodes up the DOM but
  35
+        // has no effect on moving nodes down the DOM.
  36
+        //
  37
+        // However it also deletes the text nodes under the option nodes in all
  38
+        // versions of IE. Even deep cloning doesn't fix it so we have to
  39
+        // recreate them.
  40
+        from_box.innerHTML = '';
  41
+        if (large) to_box.innerHTML = '';
  42
+
  43
+        for (var i = 0, j = SelectBox.options[id].length; i < j; i++) {
  44
+            node = SelectBox.options[id][i];
18 45
             if (node.displayed) {
19  
-                box.options[box.options.length] = new Option(node.text, node.value, false, false);
  46
+                if (webkit_repair) {
  47
+                    node.select_boxed = node.selected;
  48
+                } else if (!all && node.selected) {
  49
+                    node.select_boxed = !node.select_boxed;
  50
+                }
  51
+                node.selected = false;
  52
+                add_to = (!node.select_boxed) ? from_fragment : (large || webkit_repair) ? to_fragment : null;
  53
+                if (add_to) {
  54
+                    if (django.jQuery.browser.msie) {
  55
+                        node.appendChild(document.createTextNode(node.text_copy));
  56
+                    }
  57
+                    add_to.appendChild(node);
  58
+                }
  59
+            }
  60
+        }
  61
+
  62
+        from_box.appendChild(from_fragment); 
  63
+        from_box.selectedIndex = -1;
  64
+        to_box.appendChild(to_fragment); 
  65
+        to_box.selectedIndex = -1;
  66
+    },
  67
+    is_filter_match: function(tokens, text) {
  68
+        var token;
  69
+        for (var j = 0; (token = tokens[j]); j++) {
  70
+            if (text.toLowerCase().indexOf(token) == -1) {
  71
+                return false;
20 72
             }
21 73
         }
  74
+        return true;
22 75
     },
23 76
     filter: function(id, text) {
24 77
         // Redisplay the HTML select box, displaying only the choices containing ALL
25 78
         // the words in text. (It's an AND search.)
26  
-        var tokens = text.toLowerCase().split(/\s+/);
27  
-        var node, token;
28  
-        for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
29  
-            node.displayed = 1;
30  
-            for (var j = 0; (token = tokens[j]); j++) {
31  
-                if (node.text.toLowerCase().indexOf(token) == -1) {
32  
-                    node.displayed = 0;
33  
-                }
34  
-            }
  79
+        var tokens = text.toLowerCase().split(/\s+/),
  80
+            node;
  81
+        for (var i = 0; i < SelectBox.options[id].length; i++) {
  82
+            node = SelectBox.options[id][i];
  83
+            if (node) node.displayed = SelectBox.is_filter_match(tokens, node.text);
35 84
         }
  85
+        
36 86
         SelectBox.redisplay(id);
  87
+
  88
+        // Sometimes Chrome doesn't scroll up after a filter which makes it look
  89
+        // like there's no results even when there are
  90
+        document.getElementById(id + '_from').scrollTop = 0;
37 91
     },
38  
-    delete_from_cache: function(id, value) {
39  
-        var node, delete_index = null;
40  
-        for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
41  
-            if (node.value == value) {
42  
-                delete_index = i;
43  
-                break;
44  
-            }
  92
+    add_new: function(id, option) {
  93
+        var from_box = document.getElementById(id + '_from'),
  94
+            to_box = document.getElementById(id + '_to');
  95
+        if (django.jQuery.browser.msie) {
  96
+            option.text_copy = option.text;
  97
+            option.appendChild(document.createTextNode(option.text));
45 98
         }
46  
-        var j = SelectBox.cache[id].length - 1;
47  
-        for (var i = delete_index; i < j; i++) {
48  
-            SelectBox.cache[id][i] = SelectBox.cache[id][i+1];
49  
-        }
50  
-        SelectBox.cache[id].length--;
  99
+        option.displayed = true;
  100
+        option.select_boxed = true;
  101
+        SelectBox.add_to_options(id, option);
  102
+        // We could order alphabetically but what if the data isn't meant to be
  103
+        // alphabetical? Just adding to the end is more predictable, not to
  104
+        // mention it avoids ordering differences between browsers, databases
  105
+        // and l10n.
  106
+        option.order = SelectBox.options[id].length;
  107
+        SelectBox.insert_option(to_box, option, 0, true);
  108
+        SelectBox.replace_state();
51 109
     },
52  
-    add_to_cache: function(id, option) {
53  
-        SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1});
  110
+    add_to_options: function(id, option) {
  111
+        SelectBox.options[id].push(option);
54 112
     },
55  
-    cache_contains: function(id, value) {
56  
-        // Check if an item is contained in the cache
57  
-        var node;
58  
-        for (var i = 0; (node = SelectBox.cache[id][i]); i++) {
59  
-            if (node.value == value) {
60  
-                return true;
  113
+    insert_option: function(to_box, option, i, no_search) {
  114
+        var old_index = to_box.selectedIndex;
  115
+
  116
+        if (!no_search) {
  117
+            for (var i = i; (next_option = to_box.options[i]); i++) {
  118
+                if (next_option.order > option.order) {
  119
+                    next_option.parentNode.insertBefore(option, next_option);
  120
+                    if ((to_box.selectedIndex > -1) && (i < to_box.selectedIndex)) {
  121
+                        // Maintains the old index to prevent a jump when the box
  122
+                        // regains focus
  123
+                        to_box.selectedIndex = old_index + 1;
  124
+                    }
  125
+                    return i;
  126
+                }
61 127
             }
62 128
         }
63  
-        return false;
  129
+
  130
+        to_box.appendChild(option);
  131
+        return ++i;
64 132
     },
65  
-    move: function(from, to) {
66  
-        var from_box = document.getElementById(from);
67  
-        var to_box = document.getElementById(to);
68  
-        var option;
69  
-        for (var i = 0; (option = from_box.options[i]); i++) {
70  
-            if (option.selected && SelectBox.cache_contains(from, option.value)) {
71  
-                SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
72  
-                SelectBox.delete_from_cache(from, option.value);
  133
+    move: function(id, reverse, all) {
  134
+        var from_box = document.getElementById(id + ((!reverse) ? '_from' : '_to')),
  135
+            to_box = document.getElementById(id + ((reverse) ? '_from' : '_to')),
  136
+            num_selected = 0,
  137
+            last_compare_position = 0,
  138
+            old_selected_index = from_box.selectedIndex,
  139
+            option, compare_text, large_movement, filter_text, filter_tokens;
  140
+
  141
+        if (all) {
  142
+            num_selected = from_box.options.length;
  143
+        } else {
  144
+            if (typeof from_box.selectedOptions !== 'undefined') {
  145
+                // Fast method for browsers that support it (Chrome)
  146
+                num_selected = from_box.selectedOptions.length;
  147
+            } else {
  148
+                for (var i = 0; (option = from_box.options[i]); i++) {
  149
+                    if (option.selected) num_selected++;
  150
+                }
73 151
             }
74 152
         }
75  
-        SelectBox.redisplay(from);
76  
-        SelectBox.redisplay(to);
77  
-    },
78  
-    move_all: function(from, to) {
79  
-        var from_box = document.getElementById(from);
80  
-        var to_box = document.getElementById(to);
81  
-        var option;
82  
-        for (var i = 0; (option = from_box.options[i]); i++) {
83  
-            if (SelectBox.cache_contains(from, option.value)) {
84  
-                SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1});
85  
-                SelectBox.delete_from_cache(from, option.value);
86  
-            }
  153
+
  154
+        all = all || num_selected == from_box.options.length;
  155
+        // Eventually, moving one node at a time becomes slower than a total redisplay
  156
+        large_movement = num_selected > 1000;
  157
+
  158
+        if (reverse) {
  159
+            filter_text = document.getElementById(to_box.id.slice(0, -5) + '_input').value;
  160
+            if (filter_text) filter_tokens = filter_text.toLowerCase().split(/\s+/);
87 161
         }
88  
-        SelectBox.redisplay(from);
89  
-        SelectBox.redisplay(to);
90  
-    },
91  
-    sort: function(id) {
92  
-        SelectBox.cache[id].sort( function(a, b) {
93  
-            a = a.text.toLowerCase();
94  
-            b = b.text.toLowerCase();
95  
-            try {
96  
-                if (a > b) return 1;
97  
-                if (a < b) return -1;
  162
+
  163
+        if (all && large_movement) {
  164
+            for (var i = 0; (option = from_box.options[i]); i++) {
  165
+                option.select_boxed = !reverse;
98 166
             }
99  
-            catch (e) {
100  
-                // silently fail on IE 'unknown' exception
  167
+        }
  168
+
  169
+        if (large_movement) {
  170
+            SelectBox.redisplay(id, large_movement, all);
  171
+        } else {
  172
+            for (var i = 0; (option = from_box.options[i]); i++) {
  173
+                if (all || option.selected) {
  174
+                    option.select_boxed = !option.select_boxed;
  175
+
  176
+                    // Take the option out of the DOM otherwise setting selected to false
  177
+                    // is the slowest thing out of all this code in all browsers except Firefox
  178
+                    from_box.removeChild(option);
  179
+                    option.selected = false;
  180
+
  181
+                    // Don't add to to_box if there's a filter applied that doesn't match
  182
+                    if (!filter_tokens || SelectBox.is_filter_match(filter_tokens, option.text)) {
  183
+                        last_compare_position = SelectBox.insert_option(
  184
+                            to_box, option, last_compare_position, to_box.options.length == 0
  185
+                        );
  186
+                    }
  187
+
  188
+                    i--; // We have to decrement because we're modifying as iterating
  189
+                }
101 190
             }
102  
-            return 0;
103  
-        } );
  191
+        }
  192
+
  193
+        // This forces the list to scroll to the top of your previous selection
  194
+        // after a chunk movement. Without it you often end up in the middle 
  195
+        // of nowhere and lost.
  196
+        //
  197
+        // 13 and 70 were chosen based on the height of the boxes in the default
  198
+        // Django theme and will place the top of your previous selection in
  199
+        // about the middle. It needs a slight delay to fire properly in most
  200
+        // browsers.
  201
+        //
  202
+        // It doesn't work in Opera because Opera doesn't let you set scrollTop on
  203
+        // select elements. But Opera does almost the same by default anyway
  204
+        // (basically it won't have the -70).
  205
+        if (!django.jQuery.browser.opera && (large_movement || num_selected > 13)) {
  206
+            setTimeout(function() {
  207
+                from_box.selectedIndex = old_selected_index;
  208
+                var scroll_position = from_box.scrollTop - 70;
  209
+                from_box.selectedIndex = -1;
  210
+                from_box.scrollTop = scroll_position;
  211
+            }, 10);
  212
+        }
  213
+
  214
+        SelectBox.replace_state();
  215
+    },
  216
+    move_all: function(id, reverse) {
  217
+        SelectBox.move(id, reverse, true);
104 218
     },
105 219
     select_all: function(id) {
106 220
         var box = document.getElementById(id);
107 221
         for (var i = 0; i < box.options.length; i++) {
108  
-            box.options[i].selected = 'selected';
  222
+            box.options[i].selected = true;
109 223
         }
  224
+    },
  225
+    replace_state: function() {
  226
+        if (!django.jQuery.browser.webkit) return;
  227
+        // Make Webkit fire a distinct onpopstate on back button
  228
+        history.replaceState({}, null);
  229
+    },
  230
+    register_onpopstate: function() {
  231
+        if (SelectBox.initialised || !django.jQuery.browser.webkit) return;
  232
+
  233
+        var initial_state = {content: django.jQuery('#content').html()},
  234
+            popped = ('state' in window.history),
  235
+            state;
  236
+
  237
+        django.jQuery(window).bind('popstate', function(e) {
  238
+            if (!popped) {
  239
+                // Ignore first page loads
  240
+                popped = true;
  241
+            }
  242
+            if (e.originalEvent.state != null) {
  243
+                for (id in SelectBox.options) {
  244
+                    SelectBox.redisplay(id, false, false, true);
  245
+                }
  246
+            }
  247
+        });
110 248
     }
111 249
 }
111  django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -13,6 +13,7 @@ function findForm(node) {
13 13
 }
14 14
 
15 15
 window.SelectFilter = {
  16
+    typingTimers: new Object(),
16 17
     init: function(field_id, field_name, is_stacked, admin_static_prefix) {
17 18
         if (field_id.match(/__prefix__/)){
18 19
             // Don't intialize on empty forms.
@@ -58,15 +59,15 @@ window.SelectFilter = {
58 59
         filter_input.id = field_id + '_input';
59 60
 
60 61
         selector_available.appendChild(from_box);
61  
-        var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');
  62
+        var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link', 'tabindex', '-1');
62 63
         choose_all.className = 'selector-chooseall';
63 64
 
64 65
         // <ul class="selector-chooser">
65 66
         var selector_chooser = quickElement('ul', selector_div, '');
66 67
         selector_chooser.className = 'selector-chooser';
67  
-        var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');
  68
+        var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link', 'tabindex', '-1');
68 69
         add_link.className = 'selector-add';
69  
-        var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link');
  70
+        var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '", true); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_link', 'tabindex', '-1');
70 71
         remove_link.className = 'selector-remove';
71 72
 
72 73
         // <div class="selector-chosen">
@@ -77,23 +78,23 @@ window.SelectFilter = {
77 78
 
78 79
         var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
79 80
         to_box.className = 'filtered';
80  
-        var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');
  81
+        var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '", true); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link', 'tabindex', '-1');
81 82
         clear_all.className = 'selector-clearall';
82 83
 
83 84
         from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
84 85
 
85 86
         // Set up the JavaScript event handlers for the select box filter interface
86  
-        addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
87 87
         addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
  88
+        addEvent(from_box, 'keydown', function(e) { SelectFilter.box_key_down(e, field_id); });
  89
+        addEvent(to_box, 'keydown', function(e) { SelectFilter.box_key_down(e, field_id); });
  90
+        addEvent(from_box, 'focus', function(e) { SelectFilter.box_focus(e, field_id); });
  91
+        addEvent(to_box, 'focus', function(e) { SelectFilter.box_focus(e, field_id); });
88 92
         addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
89 93
         addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
90  
-        addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
91  
-        addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
  94
+        addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id); SelectFilter.refresh_icons(field_id); });
  95
+        addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id, true); SelectFilter.refresh_icons(field_id); });
92 96
         addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
93  
-        SelectBox.init(field_id + '_from');
94  
-        SelectBox.init(field_id + '_to');
95  
-        // Move selected from_box options to to_box
96  
-        SelectBox.move(field_id + '_from', field_id + '_to');
  97
+        SelectBox.init(field_id);
97 98
 
98 99
         if (!is_stacked) {
99 100
             // In horizontal mode, give the same height to the two boxes.
@@ -112,49 +113,71 @@ window.SelectFilter = {
112 113
         SelectFilter.refresh_icons(field_id);
113 114
     },
114 115
     refresh_icons: function(field_id) {
115  
-        var from = $('#' + field_id + '_from');
116  
-        var to = $('#' + field_id + '_to');
117  
-        var is_from_selected = from.find('option:selected').length > 0;
118  
-        var is_to_selected = to.find('option:selected').length > 0;
  116
+        var from = document.getElementById(field_id + '_from');
  117
+            to = document.getElementById(field_id + '_to'),
  118
+            is_from_selected = from.selectedIndex > -1,
  119
+            is_to_selected = to.selectedIndex > -1;
119 120
         // Active if at least one item is selected
120 121
         $('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
121 122
         $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
122 123
         // Active if the corresponding box isn't empty
123  
-        $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
124  
-        $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
  124
+        $('#' + field_id + '_add_all_link').toggleClass('active', from.options.length > 0);
  125
+        $('#' + field_id + '_remove_all_link').toggleClass('active', to.options.length > 0);
125 126
     },
126  
-    filter_key_up: function(event, field_id) {
127  
-        var from = document.getElementById(field_id + '_from');
128  
-        // don't submit form if user pressed Enter
129  
-        if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
130  
-            from.selectedIndex = 0;
131  
-            SelectBox.move(field_id + '_from', field_id + '_to');
132  
-            from.selectedIndex = 0;
133  
-            return false;
  127
+    box_focus: function(event, field_id) {
  128
+        var target = event.target || event.srcElement;
  129
+        if (target.selectedIndex < 0) {
  130
+            target.selectedIndex = 0;
134 131
         }
135  
-        var temp = from.selectedIndex;
136  
-        SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
137  
-        from.selectedIndex = temp;
138  
-        return true;
139 132
     },
140  
-    filter_key_down: function(event, field_id) {
141  
-        var from = document.getElementById(field_id + '_from');
142  
-        // right arrow -- move across
143  
-        if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
144  
-            var old_index = from.selectedIndex;
145  
-            SelectBox.move(field_id + '_from', field_id + '_to');
146  
-            from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
147  
-            return false;
  133
+    box_key_down: function(event, field_id) {
  134
+        var key = event.keyCode || event.which,
  135
+            box = event.target || event.srcElement,
  136
+            reverse = /_to$/.test(box.id);
  137
+
  138
+        if (event.shiftKey) {
  139
+            return true; // Prevent Opera's spatial navigation thing from moving options
  140
+        }
  141
+
  142
+        if (key == 32 || (!reverse && key == 39) || (reverse && key == 37)) { // Enter, space, or left/right arrow - move across
  143
+            var old_index = box.selectedIndex;
  144
+            SelectBox.move(field_id, reverse);
  145
+            // Firefox mostly has this feature by default except it's buggy at the top
  146
+            if (!$.browser.mozilla) {
  147
+                box.selectedIndex = (old_index == box.length) ? box.length - 1 : old_index;
  148
+            } else if (old_index == 0) {
  149
+                box.selectedIndex = -1;
  150
+            }
  151
+        } else {
  152
+            return true;
148 153
         }
149  
-        // down arrow -- wrap around
150  
-        if ((event.which && event.which == 40) || (event.keyCode && event.keyCode == 40)) {
151  
-            from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
  154
+
  155
+        event.preventDefault ? event.preventDefault() : event.returnValue = false; // With <= IE8 fix
  156
+    },
  157
+    filter_key_down: function(event, field_id) {
  158
+        clearTimeout(SelectFilter.typingTimers[field_id]);
  159
+
  160
+        var from = document.getElementById(field_id + '_from'),
  161
+            key = event.keyCode || event.which,
  162
+            delay = 250,
  163
+            num_options = SelectBox.options[field_id].length;
  164
+
  165
+        if (key != 8 && key != 13 && key < 46) { // Don't do anything if just passing through
  166
+            return true;
152 167
         }
153  
-        // up arrow -- wrap around
154  
-        if ((event.which && event.which == 38) || (event.keyCode && event.keyCode == 38)) {
155  
-            from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
  168
+
  169
+        if (key == 13) { // Hitting Enter is instant search
  170
+            delay = 0;
  171
+            event.preventDefault ? event.preventDefault() : event.returnValue = false; // With <= IE8 fix
  172
+        } else if (num_options > 1000) { // Large boxes have added delay for safety
  173
+            delay = 1000;
156 174
         }
157  
-        return true;
  175
+
  176
+        SelectFilter.typingTimers[field_id] = setTimeout(function() {
  177
+            SelectBox.filter(field_id, document.getElementById(field_id + '_input').value);
  178
+        }, delay);
  179
+
  180
+        return false;
158 181
     }
159 182
 }
160 183
 
6  django/contrib/admin/static/admin/js/admin/RelatedObjectLookups.js
@@ -87,11 +87,7 @@ function dismissAddAnotherPopup(win, newId, newRepr) {
87 87
             }
88 88
         }
89 89
     } else {
90  
-        var toId = name + "_to";
91  
-        elem = document.getElementById(toId);
92  
-        var o = new Option(newRepr, newId);
93  
-        SelectBox.add_to_cache(toId, o);
94  
-        SelectBox.redisplay(toId);
  90
+        SelectBox.add_new(name, new Option(newRepr, newId));
95 91
     }
96 92
     win.close();
97 93
 }
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.