Skip to content
Browse files

Dropped cache and improved keyboard accessibility

  • Loading branch information...
1 parent 9754ef9 commit 0636d5e9f72198b02c8e161cc12ae1479947f7c8 @kylemacfarlane kylemacfarlane committed Jul 22, 2012
Showing with 135 additions and 129 deletions.
  1. +67 −90 django/contrib/admin/static/admin/js/SelectBox.js
  2. +68 −39 django/contrib/admin/static/admin/js/SelectFilter2.js
View
157 django/contrib/admin/static/admin/js/SelectBox.js
@@ -1,21 +1,26 @@
var SelectBox = {
- cache: new Object(),
- initialised: new Object(),
+ options: new Object(),
init: function(id) {
- var box = document.getElementById(id),
+ var box = document.getElementById(id + '_from'),
node;
- SelectBox.cache[id] = new Array();
+ SelectBox.options[id] = new Array();
for (var i = 0; (node = box.options[i]); i++) {
node.order = i; // Record the initial order
if (django.jQuery.browser.msie) node.text_copy = node.text;
node.displayed = true;
- SelectBox.add_to_cache(id, node);
+ node.select_boxed = false;
+ SelectBox.add_to_options(id, node);
}
+ box.selectedIndex = -1;
},
- redisplay: function(box, also_wipe) {
- // Repopulate HTML select box from cache
- var fragment = document.createDocumentFragment(),
- node;
+ redisplay: function(id, both) {
+ // Repopulate HTML select box from options
+ var from_fragment = document.createDocumentFragment(),
+ to_fragment = document.createDocumentFragment(),
+ from_box = document.getElementById(id + '_from'),
+ to_box = document.getElementById(id + '_to'),
+ node, add_to;
+
// Setting innerHTML doubles the speed by making it unnecessary for the
// browser to compliment appendChild with removeChild. For example, in
@@ -25,19 +30,27 @@ var SelectBox = {
// However it also deletes the text nodes under the option nodes in all
// versions of IE. Even deep cloning doesn't fix it so we have to
// recreate them.
- box.innerHTML = '';
- if (also_wipe) also_wipe.innerHTML = '';
-
- for (var i = 0, j = SelectBox.cache[box.id].length; i < j; i++) {
- node = SelectBox.cache[box.id][i];
- if (node && node.displayed) {
- if (django.jQuery.browser.msie) {
- node.appendChild(document.createTextNode(node.text_copy));
+ from_box.innerHTML = '';
+ if (both) to_box.innerHTML = '';
+
+ for (var i = 0, j = SelectBox.options[id].length; i < j; i++) {
+ node = SelectBox.options[id][i];
+ if (node.displayed) {
+ node.selected = false;
+ add_to = (!node.select_boxed) ? from_fragment : (both) ? to_fragment : null;
+ if (add_to) {
+ if (django.jQuery.browser.msie) {
+ node.appendChild(document.createTextNode(node.text_copy));
+ }
+ add_to.appendChild(node);
}
- fragment.appendChild(node);
}
}
- box.appendChild(fragment);
+
+ from_box.appendChild(from_fragment);
+ from_box.selectedIndex = -1;
+ to_box.appendChild(to_fragment);
+ to_box.selectedIndex = -1;
},
is_filter_match: function(tokens, text) {
var token;
@@ -53,16 +66,16 @@ var SelectBox = {
// the words in text. (It's an AND search.)
var tokens = text.toLowerCase().split(/\s+/),
node;
- for (var i = 0; i < SelectBox.cache[id].length; i++) {
- node = SelectBox.cache[id][i];
+ for (var i = 0; i < SelectBox.options[id].length; i++) {
+ node = SelectBox.options[id][i];
if (node) node.displayed = SelectBox.is_filter_match(tokens, node.text);
}
- var box = document.getElementById(id);
- SelectBox.redisplay(box);
+ SelectBox.redisplay(id);
+
// Sometimes Chrome doesn't scroll up after a filter which makes it look
// like there's no results even when there are
- box.scrollTop = 0;
+ document.getElementById(id + '_from').scrollTop = 0;
},
add_new: function(id, option) {
var from_box = document.getElementById(id + '_from'),
@@ -71,35 +84,31 @@ var SelectBox = {
option.text_copy = option.text;
option.appendChild(document.createTextNode(option.text));
}
- SelectBox.add_to_cache(id + '_to', option);
+ option.displayed = true;
+ option.select_boxed = true;
+ SelectBox.add_to_options(id, option);
// We could order alphabetically but what if the data isn't meant to be
// alphabetical? Just adding to the end is more predictable, not to
// mention it avoids ordering differences between browsers, databases
// and l10n.
- option.order = to_box.options.length + from_box.options.length;
+ option.order = SelectBox.options[id].length;
SelectBox.insert_option(to_box, option, 0, true);
},
- delete_from_cache: function(id, option) {
- delete SelectBox.cache[id][option.cache_key];
- delete option.cache_key;
- },
- swap_cache: function(from, to, option) {
- SelectBox.delete_from_cache(from, option);
- SelectBox.add_to_cache(to, option);
- },
- add_to_cache: function(id, option) {
- SelectBox.cache[id].push(option);
- option.cache_key = SelectBox.cache[id].length - 1;
- },
- merge_cache: function(from, to) {
- SelectBox.cache[to] = SelectBox.cache[to].concat(SelectBox.cache[from]);
- SelectBox.cache[from] = Array();
+ add_to_options: function(id, option) {
+ SelectBox.options[id].push(option);
},
insert_option: function(to_box, option, i, no_search) {
+ var old_index = to_box.selectedIndex;
+
if (!no_search) {
for (var i = i; (next_option = to_box.options[i]); i++) {
if (next_option.order > option.order) {
next_option.parentNode.insertBefore(option, next_option);
+ if ((to_box.selectedIndex > -1) && (i < to_box.selectedIndex)) {
+ // Maintains the old index to prevent a jump when the box
+ // regains focus
+ to_box.selectedIndex = old_index + 1;
+ }
return i;
}
}
@@ -108,13 +117,11 @@ var SelectBox = {
to_box.appendChild(option);
return ++i;
},
- move: function(from, to, all) {
- var from_box = document.getElementById(from),
- to_box = document.getElementById(to),
+ move: function(id, reverse, all) {
+ var from_box = document.getElementById(id + ((!reverse) ? '_from' : '_to')),
+ to_box = document.getElementById(id + ((reverse) ? '_from' : '_to')),
num_selected = 0,
last_compare_position = 0,
- to_needs_sort = false,
- initial = typeof SelectBox.initialised[from] === 'undefined',
old_selected_index = from_box.selectedIndex,
option, compare_text, large_movement, filter_text, filter_tokens;
@@ -131,20 +138,27 @@ var SelectBox = {
}
}
+ all = all || num_selected == from_box.options.length;
// Eventually, moving one node at a time becomes slower than a total redisplay
- large_movement = num_selected > 4000;
+ large_movement = num_selected > 1000;
- if (/_from$/.test(to_box.id)) {
+ if (reverse) {
filter_text = document.getElementById(to_box.id.slice(0, -5) + '_input').value;
if (filter_text) filter_tokens = filter_text.toLowerCase().split(/\s+/);
}
if (all && large_movement) {
- SelectBox.merge_cache(from, to);
+ for (var i = 0; (option = from_box.options[i]); i++) {
+ option.select_boxed = !reverse;
+ }
+ }
+
+ if (large_movement) {
+ SelectBox.redisplay(id, true);
} else {
for (var i = 0; (option = from_box.options[i]); i++) {
if (all || option.selected) {
- SelectBox.swap_cache(from, to, option);
+ option.select_boxed = !option.select_boxed;
// Take the option out of the DOM otherwise setting selected to false
// is the slowest thing out of all this code in all browsers except Firefox
@@ -154,35 +168,15 @@ var SelectBox = {
// Don't add to to_box if there's a filter applied that doesn't match
if (!filter_tokens || SelectBox.is_filter_match(filter_tokens, option.text)) {
last_compare_position = SelectBox.insert_option(
- to_box, option, last_compare_position, initial
+ to_box, option, last_compare_position, to_box.options.length == 0
);
- } else {
- // Because we didn't move the option into the sorted HTML
- // we will need to sort the cache instead
- to_needs_sort = true;
}
i--; // We have to decrement because we're modifying as iterating
}
}
}
- SelectBox.sort(to);
-
- if (large_movement) {
- if (!initial) {
- SelectBox.sort(from);
- SelectBox.sort(to);
- }
- SelectBox.redisplay(from_box, to_box);
- SelectBox.redisplay(to_box);
- }
-
- if (initial) {
- SelectBox.initialised[from] = true;
- SelectBox.initialised[to] = true;
- }
-
// This forces the list to scroll to the top of your previous selection
// after a chunk movement. Without it you often end up in the middle
// of nowhere and lost.
@@ -204,25 +198,8 @@ var SelectBox = {
}, 10);
}
},
- move_all: function(from, to) {
- SelectBox.move(from, to, true);
- },
- sort: function(id) {
- SelectBox.cache[id].sort(function(a, b) {
- try {
- if (a.order > b.order) {
- a.cache_key++;
- return 1;
- } else if (a.order < b.order) {
- a.cache_key--;
- return -1;
- }
- }
- catch (e) {
- // silently fail on IE 'unknown' exception
- }
- return 0;
- } );
+ move_all: function(id, reverse) {
+ SelectBox.move(id, reverse, true);
},
select_all: function(id) {
var box = document.getElementById(id);
View
107 django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -59,15 +59,15 @@ window.SelectFilter = {
filter_input.id = field_id + '_input';
selector_available.appendChild(from_box);
- 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');
+ 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');
choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div, '');
selector_chooser.className = 'selector-chooser';
- 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');
+ 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');
add_link.className = 'selector-add';
- 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');
+ 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');
remove_link.className = 'selector-remove';
// <div class="selector-chosen">
@@ -78,22 +78,25 @@ window.SelectFilter = {
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
to_box.className = 'filtered';
- 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');
+ 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');
clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
// Set up the JavaScript event handlers for the select box filter interface
addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
+ addEvent(from_box, 'keydown', function(e) { SelectFilter.box_key_down(e, field_id); });
+ addEvent(to_box, 'keydown', function(e) { SelectFilter.box_key_down(e, field_id); });
+ addEvent(from_box, 'focus', function(e) { SelectFilter.box_focus(e, field_id); });
+ addEvent(to_box, 'focus', function(e) { SelectFilter.box_focus(e, field_id); });
addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
- addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
- addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
+ addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id); SelectFilter.refresh_icons(field_id); });
+ addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id, true); SelectFilter.refresh_icons(field_id); });
addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
- SelectBox.init(field_id + '_from');
- SelectBox.init(field_id + '_to');
+ SelectBox.init(field_id);
// Move selected from_box options to to_box
- SelectBox.move(field_id + '_from', field_id + '_to');
+ SelectBox.move(field_id);
if (!is_stacked) {
// In horizontal mode, give the same height to the two boxes.
@@ -112,44 +115,70 @@ window.SelectFilter = {
SelectFilter.refresh_icons(field_id);
},
refresh_icons: function(field_id) {
- var from = $('#' + field_id + '_from');
- var to = $('#' + field_id + '_to');
- var is_from_selected = from.get(0).selectedIndex > -1;
- var is_to_selected = to.get(0).selectedIndex > -1;
+ var from = document.getElementById(field_id + '_from');
+ to = document.getElementById(field_id + '_to'),
+ is_from_selected = from.selectedIndex > -1,
+ is_to_selected = to.selectedIndex > -1;
// Active if at least one item is selected
$('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
$('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
// Active if the corresponding box isn't empty
- $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
- $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
+ $('#' + field_id + '_add_all_link').toggleClass('active', from.options.length > 0);
+ $('#' + field_id + '_remove_all_link').toggleClass('active', to.options.length > 0);
+ },
+ box_focus: function(event, field_id) {
+ var target = event.target || event.srcElement;
+ if (target.selectedIndex < 0) {
+ target.selectedIndex = 0;
+ }
+ },
+ box_key_down: function(event, field_id) {
+ var key = event.keyCode || event.which,
+ box = event.target || event.srcElement,
+ reverse = /_to$/.test(box.id);
+
+ if (event.shiftKey) {
+ return true; // Prevent Opera's spacial navigation thing from moving options
+ }
+
+ if (key == 32 || (!reverse && key == 39) || (reverse && key == 37)) { // Enter, space, or left/right arrow - move across
+ var old_index = box.selectedIndex;
+ SelectBox.move(field_id, reverse);
+ // Firefox mostly has this feature by default except it's buggy at the top
+ if (!$.browser.mozilla) {
+ box.selectedIndex = (old_index == box.length) ? box.length - 1 : old_index;
+ } else if (old_index == 0) {
+ box.selectedIndex = -1;
+ }
+ } else {
+ return true;
+ }
+
+ event.preventDefault ? event.preventDefault() : event.returnValue = false; // With <= IE8 fix
},
filter_key_down: function(event, field_id) {
+ clearTimeout(SelectFilter.typingTimers[field_id]);
+
var from = document.getElementById(field_id + '_from'),
- key = event.keyCode || event.which;
-
- if (key == 39) { // Right arrow - move across
- var old_index = from.selectedIndex;
- SelectBox.move(field_id + '_from', field_id + '_to');
- from.selectedIndex = (old_index == from.length) ? from.length - 1 : old_index;
- } else if (key == 40) { // Down arrow - wrap around
- from.selectedIndex = (from.length == from.selectedIndex + 1) ? 0 : from.selectedIndex + 1;
- } else if (key == 38) { // Up arrow - wrap around
- from.selectedIndex = (from.selectedIndex == 0) ? from.length - 1 : from.selectedIndex - 1;
- } else {
- clearTimeout(SelectFilter.typingTimers[field_id]);
- var delay = 250,
- num_options = SelectBox.cache[field_id + '_from'].length;
- if (key == 13) { // Hitting Enter is instant search
- delay = 0;
- // Prevent Enter from bubbling and submitting the form. Also some <= IE8 compatibility
- event.preventDefault ? event.preventDefault() : event.returnValue = false;
- } else if (num_options > 1000) { // Large boxes have added delay for safety
- delay = 1000;
- }
- SelectFilter.typingTimers[field_id] = setTimeout(function() {
- SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value);
- }, delay);
+ key = event.keyCode || event.which,
+ delay = 250,
+ num_options = SelectBox.options[field_id].length;
+
+ if (key != 8 && key != 13 && key < 46) { // Don't do anything if just passing through
+ return true;
}
+
+ if (key == 13) { // Hitting Enter is instant search
+ delay = 0;
+ event.preventDefault ? event.preventDefault() : event.returnValue = false; // With <= IE8 fix
+ } else if (num_options > 1000) { // Large boxes have added delay for safety
+ delay = 1000;
+ }
+
+ SelectFilter.typingTimers[field_id] = setTimeout(function() {
+ SelectBox.filter(field_id, document.getElementById(field_id + '_input').value);
+ }, delay);
+
return false;
}
}

0 comments on commit 0636d5e

Please sign in to comment.
Something went wrong with that request. Please try again.