Fixed #16059 -- Improved the usability of the admin's vertical and horizontal "filter" widgets, in particular by providing a better visual representation of the buttons' enabled and disabled states, and by providing more elaborate, yet less cluttered, help texts.

Note that this commit is an exception to the current tacit rule that javascript code changes should be avoided until a proper javascript testing framework for Django core is in place. This exception is commanded by the fact that it is to fix a recognized usability issue and that the patch has been (manually) extensively tested in IE6+, Chrome, Safari, Firefox and Opera.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16714 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Julien Phalip 2011-08-31 08:07:07 +00:00
parent 3a2e15abd9
commit 71f017b2a6
10 changed files with 115 additions and 34 deletions

View File

@ -17,6 +17,10 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
.selector-chosen select {
border-top: none;
}
.selector-available h2, .selector-chosen h2 { .selector-available h2, .selector-chosen h2 {
border: 1px solid #ccc; border: 1px solid #ccc;
} }
@ -37,8 +41,9 @@
text-align: left; text-align: left;
} }
.selector .selector-chosen .selector-filter { .selector .selector-filter label {
padding: 4px 5px; width: 16px;
padding: 2px;
} }
.selector .selector-available input { .selector .selector-available input {
@ -50,7 +55,7 @@
width: 22px; width: 22px;
height: 50px; height: 50px;
background: url(../img/chooser-bg.gif) top center no-repeat; background: url(../img/chooser-bg.gif) top center no-repeat;
margin: 8em 3px 0 3px; margin: 10em 5px 0 5px;
padding: 0; padding: 0;
} }
@ -61,7 +66,7 @@
} }
.selector select { .selector select {
margin-bottom: 5px; margin-bottom: 10px;
margin-top: 0; margin-top: 0;
} }
@ -74,38 +79,66 @@
} }
.selector-add { .selector-add {
background: url(../img/selector-add.gif) top center no-repeat; background: url(../img/selector-icons.gif) 0 -161px no-repeat;
cursor: default;
margin-bottom: 2px; margin-bottom: 2px;
} }
.active.selector-add {
background: url(../img/selector-icons.gif) 0 -187px no-repeat;
cursor: pointer;
}
.selector-remove { .selector-remove {
background: url(../img/selector-remove.gif) top center no-repeat; background: url(../img/selector-icons.gif) 0 -109px no-repeat;
cursor: default;
}
.active.selector-remove {
background: url(../img/selector-icons.gif) 0 -135px no-repeat;
cursor: pointer;
} }
a.selector-chooseall, a.selector-clearall { a.selector-chooseall, a.selector-clearall {
display: block; display: inline-block;
width: 6em;
text-align: left; text-align: left;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
font-weight: bold; font-weight: bold;
color: #666; color: #666;
}
a.selector-chooseall {
padding: 3px 18px 3px 0;
}
a.selector-clearall {
padding: 3px 0 3px 18px; padding: 3px 0 3px 18px;
} }
a.selector-chooseall:hover, a.selector-clearall:hover { a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
color: #036; color: #036;
} }
a.selector-chooseall { a.selector-chooseall {
width: 7em; background: url(../img/selector-icons.gif) right -263px no-repeat;
background: url(../img/selector-addall.gif) left center no-repeat; cursor: default;
}
a.active.selector-chooseall {
background: url(../img/selector-icons.gif) right -289px no-repeat;
cursor: pointer;
} }
a.selector-clearall { a.selector-clearall {
background: url(../img/selector-removeall.gif) left center no-repeat; background: url(../img/selector-icons.gif) left -211px no-repeat;
cursor: default;
} }
a.active.selector-clearall {
background: url(../img/selector-icons.gif) left -237px no-repeat;
cursor: pointer;
}
/* STACKED SELECTORS */ /* STACKED SELECTORS */
@ -148,13 +181,24 @@ a.selector-clearall {
} }
.stacked .selector-add { .stacked .selector-add {
background-image: url(../img/selector_stacked-add.gif); background: url(../img/selector-icons.gif) 0 -57px no-repeat;
cursor: default;
}
.stacked .active.selector-add {
background: url(../img/selector-icons.gif) 0 -83px no-repeat;
cursor: pointer;
} }
.stacked .selector-remove { .stacked .selector-remove {
background-image: url(../img/selector_stacked-remove.gif); background: url(../img/selector-icons.gif) 0 -5px no-repeat;
cursor: default;
} }
.stacked .active.selector-remove {
background: url(../img/selector-icons.gif) 0 -31px no-repeat;
cursor: pointer;
}
/* DATE AND TIME */ /* DATE AND TIME */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 B

View File

@ -5,7 +5,7 @@ Different than SelectFilter because this is coupled to the admin framework.
Requires core.js, SelectBox.js and addevent.js. Requires core.js, SelectBox.js and addevent.js.
*/ */
(function($) {
function findForm(node) { function findForm(node) {
// returns the node of the form containing the given node // returns the node of the form containing the given node
if (node.tagName.toLowerCase() != 'form') { if (node.tagName.toLowerCase() != 'form') {
@ -14,7 +14,7 @@ function findForm(node) {
return node; return node;
} }
var SelectFilter = { window.SelectFilter = {
init: function(field_id, field_name, is_stacked, admin_media_prefix) { init: function(field_id, field_name, is_stacked, admin_media_prefix) {
if (field_id.match(/__prefix__/)){ if (field_id.match(/__prefix__/)){
// Don't intialize on empty forms. // Don't intialize on empty forms.
@ -44,41 +44,42 @@ var SelectFilter = {
// <div class="selector-available"> // <div class="selector-available">
var selector_available = quickElement('div', selector_div, ''); var selector_available = quickElement('div', selector_div, '');
selector_available.className = 'selector-available'; selector_available.className = 'selector-available';
quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name])); var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
var filter_p = quickElement('p', selector_available, ''); quickElement('img', title_available, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of available %s. You may add some by selecting them below and then clicking the "Add" button.'), [field_name]));
var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
filter_p.className = 'selector-filter'; filter_p.className = 'selector-filter';
var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input", 'style', 'width:16px;padding:2px'); var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");
var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/selector-search.gif'); var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/selector-search.gif', 'class', 'help-tooltip', 'alt', '', 'title', interpolate(gettext("Type into the filter box to narrow down the list of available %s."), [field_name]));
search_selector_img.alt = gettext("Filter");
filter_p.appendChild(document.createTextNode(' ')); filter_p.appendChild(document.createTextNode(' '));
var filter_input = quickElement('input', filter_p, '', 'type', 'text'); var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
filter_input.id = field_id + '_input'; filter_input.id = field_id + '_input';
selector_available.appendChild(from_box); selector_available.appendChild(from_box);
var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()'); var choose_all = quickElement('a', selector_available, gettext('Add all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_all_link');
choose_all.className = 'selector-chooseall'; choose_all.className = 'selector-chooseall';
// <ul class="selector-chooser"> // <ul class="selector-chooser">
var selector_chooser = quickElement('ul', selector_div, ''); var selector_chooser = quickElement('ul', selector_div, '');
selector_chooser.className = 'selector-chooser'; selector_chooser.className = 'selector-chooser';
var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()'); var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'title', gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_add_link');
add_link.className = 'selector-add'; add_link.className = 'selector-add';
var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()'); 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');
remove_link.className = 'selector-remove'; remove_link.className = 'selector-remove';
// <div class="selector-chosen"> // <div class="selector-chosen">
var selector_chosen = quickElement('div', selector_div, ''); var selector_chosen = quickElement('div', selector_div, '');
selector_chosen.className = 'selector-chosen'; selector_chosen.className = 'selector-chosen';
quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name])); var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Added %s') + ' ', [field_name]));
var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click ')); quickElement('img', title_chosen, '', 'src', admin_media_prefix + 'img/icon-unknown.gif', 'width', '10', 'height', '10', 'class', 'help help-tooltip', 'title', interpolate(gettext('This is the list of added %s. You may remove some by selecting them below and then clicking the "Remove" button.'), [field_name]));
selector_filter.className = 'selector-filter';
quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? 'img/selector_stacked-add.gif':'img/selector-add.gif'), 'alt', 'Add');
var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name')); 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'; to_box.className = 'filtered';
var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()'); var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from"); SelectFilter.refresh_icons("' + field_id + '");})()', 'id', field_id + '_remove_all_link');
clear_all.className = 'selector-clearall'; clear_all.className = 'selector-clearall';
from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
@ -86,16 +87,38 @@ var SelectFilter = {
// Set up the JavaScript event handlers for the select box filter interface // Set up the JavaScript event handlers for the select box filter interface
addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); }); addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); }); 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(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
SelectBox.init(field_id + '_from'); SelectBox.init(field_id + '_from');
SelectBox.init(field_id + '_to'); SelectBox.init(field_id + '_to');
// Move selected from_box options to to_box // Move selected from_box options to to_box
SelectBox.move(field_id + '_from', field_id + '_to'); SelectBox.move(field_id + '_from', field_id + '_to');
if (!is_stacked) {
// In horizontal mode, give the same height to the two boxes.
$(to_box).height($(filter_p).outerHeight() + $(from_box).outerHeight());
}
// Initial icon refresh
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.find('option:selected').length > 0;
var is_to_selected = to.find('option:selected').length > 0;
// 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);
}, },
filter_key_up: function(event, field_id) { filter_key_up: function(event, field_id) {
from = document.getElementById(field_id + '_from'); var from = document.getElementById(field_id + '_from');
// don't submit form if user pressed Enter // don't submit form if user pressed Enter
if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) { if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
from.selectedIndex = 0; from.selectedIndex = 0;
@ -109,7 +132,7 @@ var SelectFilter = {
return true; return true;
}, },
filter_key_down: function(event, field_id) { filter_key_down: function(event, field_id) {
from = document.getElementById(field_id + '_from'); var from = document.getElementById(field_id + '_from');
// right arrow -- move across // right arrow -- move across
if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) { if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
var old_index = from.selectedIndex; var old_index = from.selectedIndex;
@ -128,3 +151,5 @@ var SelectFilter = {
return true; return true;
} }
} }
})(django.jQuery);

View File

@ -328,6 +328,18 @@ In case your ``ADMIN_MEDIA_PREFIX`` is set to an own domain (e.g.
that path. The files were moved from :file:`django/contrib/admin/media/` that path. The files were moved from :file:`django/contrib/admin/media/`
to :file:`django/contrib/admin/static/admin/`. to :file:`django/contrib/admin/static/admin/`.
Removed admin icons
~~~~~~~~~~~~~~~~~~~
As part of an effort to improve the performance and usability of the admin's
vertical and horizontal "filter" widgets, some icon files were removed and
grouped into a single sprite file (``selector-icons.gif``):
``selector-add.gif``, ``selector-addall.gif``, ``selector-remove.gif``,
``selector-removeall.gif``, ``selector_stacked-add.gif`` and
``selector_stacked-remove.gif``. If you used those icons to customize the
admin then you will want to replace them with your own icons or retrieve them
from a previous release.
Compatibility with old signed data Compatibility with old signed data
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~