Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #16059 -- Improved the usability of the admin's vertical and ho…

…rizontal "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
  • Loading branch information...
commit 71f017b2a6677538f8c3578e33859d4586b5bea1 1 parent 3a2e15a
Julien Phalip authored August 31, 2011
72  django/contrib/admin/static/admin/css/widgets.css
@@ -17,6 +17,10 @@
17 17
     margin-bottom: 5px;
18 18
 }
19 19
 
  20
+.selector-chosen select {
  21
+    border-top: none;
  22
+}
  23
+
20 24
 .selector-available h2, .selector-chosen h2 {
21 25
     border: 1px solid #ccc;
22 26
 }
@@ -37,8 +41,9 @@
37 41
     text-align: left;
38 42
 }
39 43
 
40  
-.selector .selector-chosen .selector-filter {
41  
-    padding: 4px 5px;
  44
+.selector .selector-filter label {
  45
+    width: 16px;
  46
+    padding: 2px;
42 47
 }
43 48
 
44 49
 .selector .selector-available input {
@@ -50,7 +55,7 @@
50 55
     width: 22px;
51 56
     height: 50px;
52 57
     background: url(../img/chooser-bg.gif) top center no-repeat;
53  
-    margin: 8em 3px 0 3px;
  58
+    margin: 10em 5px 0 5px;
54 59
     padding: 0;
55 60
 }
56 61
 
@@ -61,7 +66,7 @@
61 66
 }
62 67
 
63 68
 .selector select {
64  
-    margin-bottom: 5px;
  69
+    margin-bottom: 10px;
65 70
     margin-top: 0;
66 71
 }
67 72
 
@@ -74,38 +79,66 @@
74 79
 }
75 80
 
76 81
 .selector-add {
77  
-    background: url(../img/selector-add.gif) top center no-repeat;
  82
+    background: url(../img/selector-icons.gif) 0 -161px no-repeat;
  83
+    cursor: default;
78 84
     margin-bottom: 2px;
79 85
 }
80 86
 
  87
+.active.selector-add {
  88
+    background: url(../img/selector-icons.gif) 0 -187px no-repeat;
  89
+    cursor: pointer;
  90
+}
  91
+
81 92
 .selector-remove {
82  
-    background: url(../img/selector-remove.gif) top center no-repeat;
  93
+    background: url(../img/selector-icons.gif) 0 -109px no-repeat;
  94
+    cursor: default;
  95
+}
  96
+
  97
+.active.selector-remove {
  98
+    background: url(../img/selector-icons.gif) 0 -135px no-repeat;
  99
+    cursor: pointer;
83 100
 }
84 101
 
85 102
 a.selector-chooseall, a.selector-clearall {
86  
-    display: block;
87  
-    width: 6em;
  103
+    display: inline-block;
88 104
     text-align: left;
89 105
     margin-left: auto;
90 106
     margin-right: auto;
91 107
     font-weight: bold;
92 108
     color: #666;
  109
+}
  110
+
  111
+a.selector-chooseall {
  112
+    padding: 3px 18px 3px 0;
  113
+}
  114
+
  115
+a.selector-clearall {
93 116
     padding: 3px 0 3px 18px;
94 117
 }
95 118
 
96  
-a.selector-chooseall:hover, a.selector-clearall:hover {
  119
+a.active.selector-chooseall:hover, a.active.selector-clearall:hover {
97 120
     color: #036;
98 121
 }
99 122
 
100 123
 a.selector-chooseall {
101  
-    width: 7em;
102  
-    background: url(../img/selector-addall.gif) left center no-repeat;
  124
+    background: url(../img/selector-icons.gif) right -263px no-repeat;
  125
+    cursor: default;
  126
+}
  127
+
  128
+a.active.selector-chooseall {
  129
+    background: url(../img/selector-icons.gif) right -289px no-repeat;
  130
+    cursor: pointer;
103 131
 }
104 132
 
105 133
 a.selector-clearall {
106  
-    background: url(../img/selector-removeall.gif) left center no-repeat;
  134
+    background: url(../img/selector-icons.gif) left -211px no-repeat;
  135
+    cursor: default;
107 136
 }
108 137
 
  138
+a.active.selector-clearall {
  139
+    background: url(../img/selector-icons.gif) left -237px no-repeat;
  140
+    cursor: pointer;
  141
+}
109 142
 
110 143
 /* STACKED SELECTORS */
111 144
 
@@ -148,13 +181,24 @@ a.selector-clearall {
148 181
 }
149 182
 
150 183
 .stacked .selector-add {
151  
-    background-image: url(../img/selector_stacked-add.gif);
  184
+    background: url(../img/selector-icons.gif) 0 -57px no-repeat;
  185
+    cursor: default;
  186
+}
  187
+
  188
+.stacked .active.selector-add {
  189
+    background: url(../img/selector-icons.gif) 0 -83px no-repeat;
  190
+    cursor: pointer;
152 191
 }
153 192
 
154 193
 .stacked .selector-remove {
155  
-    background-image: url(../img/selector_stacked-remove.gif);
  194
+    background: url(../img/selector-icons.gif) 0 -5px no-repeat;
  195
+    cursor: default;
156 196
 }
157 197
 
  198
+.stacked .active.selector-remove {
  199
+    background: url(../img/selector-icons.gif) 0 -31px no-repeat;
  200
+    cursor: pointer;
  201
+}
158 202
 
159 203
 /* DATE AND TIME */
160 204
 
BIN  django/contrib/admin/static/admin/img/selector-add.gif
BIN  django/contrib/admin/static/admin/img/selector-addall.gif
BIN  django/contrib/admin/static/admin/img/selector-icons.gif
BIN  django/contrib/admin/static/admin/img/selector-remove.gif
BIN  django/contrib/admin/static/admin/img/selector-removeall.gif
BIN  django/contrib/admin/static/admin/img/selector_stacked-add.gif
BIN  django/contrib/admin/static/admin/img/selector_stacked-remove.gif
65  django/contrib/admin/static/admin/js/SelectFilter2.js
@@ -5,7 +5,7 @@ Different than SelectFilter because this is coupled to the admin framework.
5 5
 
6 6
 Requires core.js, SelectBox.js and addevent.js.
7 7
 */
8  
-
  8
+(function($) {
9 9
 function findForm(node) {
10 10
     // returns the node of the form containing the given node
11 11
     if (node.tagName.toLowerCase() != 'form') {
@@ -14,7 +14,7 @@ function findForm(node) {
14 14
     return node;
15 15
 }
16 16
 
17  
-var SelectFilter = {
  17
+window.SelectFilter = {
18 18
     init: function(field_id, field_name, is_stacked, admin_media_prefix) {
19 19
         if (field_id.match(/__prefix__/)){
20 20
             // Don't intialize on empty forms.
@@ -44,41 +44,42 @@ var SelectFilter = {
44 44
         // <div class="selector-available">
45 45
         var selector_available = quickElement('div', selector_div, '');
46 46
         selector_available.className = 'selector-available';
47  
-        quickElement('h2', selector_available, interpolate(gettext('Available %s'), [field_name]));
48  
-        var filter_p = quickElement('p', selector_available, '');
  47
+        var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name]));
  48
+        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]));
  49
+
  50
+        var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter');
49 51
         filter_p.className = 'selector-filter';
50 52
 
51  
-        var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input", 'style', 'width:16px;padding:2px');
  53
+        var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + "_input");
52 54
 
53  
-        var search_selector_img = quickElement('img', search_filter_label, '', 'src', admin_media_prefix + 'img/selector-search.gif');
54  
-        search_selector_img.alt = gettext("Filter");
  55
+        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]));
55 56
 
56 57
         filter_p.appendChild(document.createTextNode(' '));
57 58
 
58  
-        var filter_input = quickElement('input', filter_p, '', 'type', 'text');
  59
+        var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter"));
59 60
         filter_input.id = field_id + '_input';
  61
+
60 62
         selector_available.appendChild(from_box);
61  
-        var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'href', 'javascript: (function(){ SelectBox.move_all("' + field_id + '_from", "' + field_id + '_to"); })()');
  63
+        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');
62 64
         choose_all.className = 'selector-chooseall';
63 65
 
64 66
         // <ul class="selector-chooser">
65 67
         var selector_chooser = quickElement('ul', selector_div, '');
66 68
         selector_chooser.className = 'selector-chooser';
67  
-        var add_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Add'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_from","' + field_id + '_to");})()');
  69
+        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');
68 70
         add_link.className = 'selector-add';
69  
-        var remove_link = quickElement('a', quickElement('li', selector_chooser, ''), gettext('Remove'), 'href', 'javascript: (function(){ SelectBox.move("' + field_id + '_to","' + field_id + '_from");})()');
  71
+        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 72
         remove_link.className = 'selector-remove';
71 73
 
72 74
         // <div class="selector-chosen">
73 75
         var selector_chosen = quickElement('div', selector_div, '');
74 76
         selector_chosen.className = 'selector-chosen';
75  
-        quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s'), [field_name]));
76  
-        var selector_filter = quickElement('p', selector_chosen, gettext('Select your choice(s) and click '));
77  
-        selector_filter.className = 'selector-filter';
78  
-        quickElement('img', selector_filter, '', 'src', admin_media_prefix + (is_stacked ? 'img/selector_stacked-add.gif':'img/selector-add.gif'), 'alt', 'Add');
  77
+        var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Added %s') + ' ', [field_name]));
  78
+        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]));
  79
+
79 80
         var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name'));
80 81
         to_box.className = 'filtered';
81  
-        var clear_all = quickElement('a', selector_chosen, gettext('Clear all'), 'href', 'javascript: (function() { SelectBox.move_all("' + field_id + '_to", "' + field_id + '_from");})()');
  82
+        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');
82 83
         clear_all.className = 'selector-clearall';
83 84
 
84 85
         from_box.setAttribute('name', from_box.getAttribute('name') + '_old');
@@ -86,16 +87,38 @@ var SelectFilter = {
86 87
         // Set up the JavaScript event handlers for the select box filter interface
87 88
         addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); });
88 89
         addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); });
89  
-        addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); });
90  
-        addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); });
  90
+        addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
  91
+        addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id) });
  92
+        addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); });
  93
+        addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); });
91 94
         addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); });
92 95
         SelectBox.init(field_id + '_from');
93 96
         SelectBox.init(field_id + '_to');
94 97
         // Move selected from_box options to to_box
95 98
         SelectBox.move(field_id + '_from', field_id + '_to');
  99
+
  100
+        if (!is_stacked) {
  101
+            // In horizontal mode, give the same height to the two boxes.
  102
+            $(to_box).height($(filter_p).outerHeight() + $(from_box).outerHeight());
  103
+        }
  104
+
  105
+        // Initial icon refresh
  106
+        SelectFilter.refresh_icons(field_id);
  107
+    },
  108
+    refresh_icons: function(field_id) {
  109
+        var from = $('#' + field_id + '_from');
  110
+        var to = $('#' + field_id + '_to');
  111
+        var is_from_selected = from.find('option:selected').length > 0;
  112
+        var is_to_selected = to.find('option:selected').length > 0;
  113
+        // Active if at least one item is selected
  114
+        $('#' + field_id + '_add_link').toggleClass('active', is_from_selected);
  115
+        $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected);
  116
+        // Active if the corresponding box isn't empty
  117
+        $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0);
  118
+        $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0);
96 119
     },
97 120
     filter_key_up: function(event, field_id) {
98  
-        from = document.getElementById(field_id + '_from');
  121
+        var from = document.getElementById(field_id + '_from');
99 122
         // don't submit form if user pressed Enter
100 123
         if ((event.which && event.which == 13) || (event.keyCode && event.keyCode == 13)) {
101 124
             from.selectedIndex = 0;
@@ -109,7 +132,7 @@ var SelectFilter = {
109 132
         return true;
110 133
     },
111 134
     filter_key_down: function(event, field_id) {
112  
-        from = document.getElementById(field_id + '_from');
  135
+        var from = document.getElementById(field_id + '_from');
113 136
         // right arrow -- move across
114 137
         if ((event.which && event.which == 39) || (event.keyCode && event.keyCode == 39)) {
115 138
             var old_index = from.selectedIndex;
@@ -128,3 +151,5 @@ var SelectFilter = {
128 151
         return true;
129 152
     }
130 153
 }
  154
+
  155
+})(django.jQuery);
12  docs/releases/1.4.txt
@@ -328,6 +328,18 @@ In case your ``ADMIN_MEDIA_PREFIX`` is set to an own domain (e.g.
328 328
     that path. The files were moved from :file:`django/contrib/admin/media/`
329 329
     to :file:`django/contrib/admin/static/admin/`.
330 330
 
  331
+Removed admin icons
  332
+~~~~~~~~~~~~~~~~~~~
  333
+
  334
+As part of an effort to improve the performance and usability of the admin's
  335
+vertical and horizontal "filter" widgets, some icon files were removed and
  336
+grouped into a single sprite file (``selector-icons.gif``):
  337
+``selector-add.gif``, ``selector-addall.gif``, ``selector-remove.gif``,
  338
+``selector-removeall.gif``, ``selector_stacked-add.gif`` and
  339
+``selector_stacked-remove.gif``. If you used those icons to customize the
  340
+admin then you will want to replace them with your own icons or retrieve them
  341
+from a previous release.
  342
+
331 343
 Compatibility with old signed data
332 344
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
333 345
 

0 notes on commit 71f017b

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