Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Simplified the admin changelist multi-sort interface specifically by …

…removing the popup window, adding explicit tooltip help texts, improving the hover visual states and allowing all operations (i.e. removing a column from sorting and toggling the sorting with and without changing the sorting priority) to be actionable with just one click. Many thanks to Idan Gazit for the feedback and direction. Refs #16212.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16899 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit f2ed107b079b050950e2fc5f2b689ca553ae12f5 1 parent b1c3174
Julien Phalip authored September 24, 2011
89  django/contrib/admin/static/admin/css/base.css
@@ -309,77 +309,84 @@ tr.alt {
309 309
 
310 310
 /* SORTABLE TABLES */
311 311
 
  312
+thead th {
  313
+    padding: 0;
  314
+    line-height: normal;
  315
+}
  316
+
312 317
 thead th a:link, thead th a:visited {
313 318
     color: #666;
314  
-    display: block;
315 319
 }
316 320
 
317 321
 thead th.sorted {
318 322
     background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
319 323
 }
320 324
 
321  
-table thead th.sorted a {
322  
-    padding-right: 13px;
  325
+table thead th .text span {
  326
+    padding: 2px 5px;
  327
+    display:block;
323 328
 }
324 329
 
325  
-table thead th.ascending a {
326  
-    background: url(../img/arrow-up.gif) right .4em no-repeat;
  330
+table thead th .text a {
  331
+    display: block;
  332
+    cursor: pointer;
  333
+    padding: 2px 5px;
327 334
 }
328 335
 
329  
-table thead th.descending a {
330  
-    background: url(../img/arrow-down.gif) right .4em no-repeat;
  336
+table thead th.sortable:hover {
  337
+    background: white url(../img/nav-bg-reverse.gif) 0 -5px repeat-x;
331 338
 }
332 339
 
333  
-table thead th.sorted a span.text {
334  
-   display: block;
335  
-   float: left;
336  
-   cursor: pointer; /* IE needs this */
  340
+thead th.sorted a.sortremove {
  341
+    visibility: hidden;
337 342
 }
338 343
 
339  
-table thead th.sorted a span.sortpos {
340  
-   display: block;
341  
-   float: right;
342  
-   font-size: .6em;
343  
-   text-align: right;
344  
-   cursor: pointer; /* IE needs this */
  344
+table thead th.sorted:hover a.sortremove {
  345
+    visibility: visible;
345 346
 }
346 347
 
347  
-table thead th.sorted a img {
348  
-   vertical-align: top;
  348
+table thead th.sorted .sortoptions {
  349
+    display: block;
  350
+    padding: 4px 5px 0 5px;
  351
+    float: right;
  352
+    text-align: right;
349 353
 }
350 354
 
351  
-table thead th.sorted a span.clear {
352  
-   display: block;
353  
-   clear: both;
  355
+table thead th.sorted .sortpriority {
  356
+    font-size: .8em;
  357
+    min-width: 12px;
  358
+    text-align: center;
  359
+    vertical-align: top;
354 360
 }
355 361
 
356  
-#sorting-popup-div {
357  
-    display: none;
358  
-    position: absolute;
359  
-    background-color: white;
360  
-    border: 1px solid #ddd;
361  
-    z-index: 2000; /* more than filters on right */
  362
+table thead th.sorted .sortoptions a {
  363
+    width: 14px;
  364
+    height: 12px;
  365
+    display: inline-block;
362 366
 }
363 367
 
364  
-#sorting-popup-div table {
365  
-    border-right: 0px;
366  
-    border-left: 0px;
  368
+table thead th.sorted .sortoptions a.sortremove {
  369
+    background: url(../img/sorting-icons.gif) -4px -5px no-repeat;
367 370
 }
368 371
 
369  
-#sorting-popup-div .reset {
370  
-    text-align: center;
  372
+table thead th.sorted .sortoptions a.sortremove:hover {
  373
+    background: url(../img/sorting-icons.gif) -4px -27px no-repeat;
371 374
 }
372 375
 
373  
-#sorting-popup-div .cancel {
374  
-    font-size: 10px;
375  
-    background: #e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x;
376  
-    border-top: 1px solid #ddd;
377  
-    text-align: center;
  376
+table thead th.sorted .sortoptions a.ascending {
  377
+    background: url(../img/sorting-icons.gif) -5px -50px no-repeat;
378 378
 }
379 379
 
380  
-#sorting-popup-div .cancel a {
381  
-    width: 100%;
382  
-    display: block;
  380
+table thead th.sorted .sortoptions a.ascending:hover {
  381
+    background: url(../img/sorting-icons.gif) -5px -72px no-repeat;
  382
+}
  383
+
  384
+table thead th.sorted .sortoptions a.descending {
  385
+    background: url(../img/sorting-icons.gif) -5px -94px no-repeat;
  386
+}
  387
+
  388
+table thead th.sorted .sortoptions a.descending:hover {
  389
+    background: url(../img/sorting-icons.gif) -5px -115px no-repeat;
383 390
 }
384 391
 
385 392
 /* ORDERABLE TABLES */
17  django/contrib/admin/static/admin/css/rtl.css
@@ -80,22 +80,7 @@ div.breadcrumbs {
80 80
 
81 81
 /* SORTABLE TABLES */
82 82
 
83  
-
84  
-table thead th.sorted a {
85  
-    padding-left: 13px;
86  
-    padding-right: 0px;
87  
-}
88  
-
89  
-table thead th.ascending a,
90  
-table thead th.descending a {
91  
-    background-position: left;
92  
-}
93  
-
94  
-table thead th.sorted a span.text {
95  
-   float: right;
96  
-}
97  
-
98  
-table thead th.sorted a span.sortpos {
  83
+table thead th.sorted .sortoptions {
99 84
    float: left;
100 85
 }
101 86
 
BIN  django/contrib/admin/static/admin/img/arrow-down.gif
BIN  django/contrib/admin/static/admin/img/arrow-up.gif
BIN  django/contrib/admin/static/admin/img/icon_cog.gif
BIN  django/contrib/admin/static/admin/img/sorting-icons.gif
106  django/contrib/admin/templates/admin/change_list_results.html
@@ -11,15 +11,17 @@
11 11
 <tr>
12 12
 {% for header in result_headers %}
13 13
 <th scope="col" {{ header.class_attrib }}>
14  
-  {% if header.sortable %}<a href="{{ header.url_primary }}">{% endif %}
15  
-  <span class="text">{{ header.text|capfirst }}</span>
16  
-  {% if header.sortable %}
17  
-    {% if header.sort_pos > 0 %}<span class="sortpos">
18  
-      {% if header.sort_pos == 1 %}<img id="primary-sort-icon" src="{% static "admin/img/icon_cog.gif" %}" alt="" />&nbsp;{% endif %}
19  
-      {{ header.sort_pos }}</span>
20  
-    {% endif %}
21  
-    <span class="clear"></span></a>
22  
-  {% endif %}
  14
+   {% if header.sortable %}
  15
+     {% if header.sort_priority > 0 %}
  16
+       <div class="sortoptions">
  17
+         <a class="sortremove" href="{{ header.url_remove }}" title="{% trans "Remove from sorting" %}"></a>
  18
+         {% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktrans with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktrans %}">{{ header.sort_priority }}</span>{% endif %}
  19
+         <a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% trans "Toggle sorting" %}"></a>
  20
+       </div>
  21
+     {% endif %}
  22
+   {% endif %}
  23
+   <div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
  24
+   <div class="clear"></div>
23 25
 </th>{% endfor %}
24 26
 </tr>
25 27
 </thead>
@@ -33,90 +35,4 @@
33 35
 </tbody>
34 36
 </table>
35 37
 </div>
36  
-
37  
-{# Sorting popup: #}
38  
-<div id="sorting-popup-div">
39  
-<table>
40  
-  <caption>
41  
-   {% trans "Sorting by:" %}
42  
-  </caption>
43  
-  <tbody>
44  
-  {% for header in result_headers|dictsort:"sort_pos" %}
45  
-    {% if header.sort_pos > 0 %}
46  
-    <tr>
47  
-      <td>{{ header.sort_pos }}</td>
48  
-      <td>{{ header.text|capfirst }}</td>
49  
-      <td>{% if header.ascending %}{% trans "ascending" %}{% else %}{% trans "descending" %}{% endif %}</td>
50  
-      <td><a href="{{ header.url_toggle }}">{% trans "toggle" %}</a></td>
51  
-      <td><a href="{{ header.url_remove }}">{% trans "remove" %}</a></td>
52  
-    </tr>
53  
-    {% endif %}
54  
-  {% endfor %}
55  
-  </tbody>
56  
-</table>
57  
-<div class="reset"><a href="{{ reset_sorting_url }}">{% trans "Reset sorting" %}</a></div>
58  
-<div class="cancel"><a href="javascript:void" id="sorting-popup-dismiss">{% trans "Cancel" %}</a></div>
59  
-</div>
60  
-<script type="text/javascript">
61  
-<!--
62  
-(function($) {
63  
-    $(document).ready(function() {
64  
-        var popup = $('#sorting-popup-div');
65  
-        var img = $('#primary-sort-icon');
66  
-        /* These next lines seems necessary to prime the popup: */
67  
-        popup.offset({left:0, top:-1000});
68  
-        popup.show();
69  
-        if ($.browser.msie) {
70  
-            // Can't find a way to make IE autosize the div.
71  
-            popup.width(300);
72  
-        }
73  
-        var popupWidth = popup.width();
74  
-        popup.hide();
75  
-
76  
-        var visible = false;
77  
-
78  
-        var escHandler = function(ev) {
79  
-            if (ev.which == 27) {
80  
-                hidePopup();
81  
-                ev.preventDefault();
82  
-            }
83  
-        };
84  
-
85  
-        var showPopup = function() {
86  
-            var pos = img.offset();
87  
-            pos.top += img.height();
88  
-            if (pos.left + popupWidth >
89  
-                $(window).width()) {
90  
-                pos.left -= popupWidth;
91  
-            }
92  
-            popup.show();
93  
-            popup.offset(pos);
94  
-            visible = true;
95  
-            $(document).bind('keyup', escHandler);
96  
-        };
97  
-
98  
-        var hidePopup = function() {
99  
-            popup.hide();
100  
-            visible = false;
101  
-            $(document).unbind('keyup', escHandler);
102  
-        };
103  
-
104  
-        $('#primary-sort-icon').click(function(ev) {
105  
-            ev.preventDefault();
106  
-            if (visible) {
107  
-                hidePopup();
108  
-            } else {
109  
-                showPopup();
110  
-            }
111  
-        });
112  
-
113  
-        $('#sorting-popup-dismiss').click(function(ev) {
114  
-            hidePopup();
115  
-            ev.preventDefault()
116  
-        });
117  
-    });
118  
-})(django.jQuery);
119  
-//-->
120  
-</script>
121  
-
122 38
 {% endif %}
27  django/contrib/admin/templatetags/admin_list.py
@@ -83,7 +83,6 @@ def result_headers(cl):
83 83
     """
84 84
     ordering_field_columns = cl.get_ordering_field_columns()
85 85
     for i, field_name in enumerate(cl.list_display):
86  
-        admin_order_field = None
87 86
         text, attr = label_for_field(field_name, cl.model,
88 87
             model_admin = cl.model_admin,
89 88
             return_attr = True
@@ -95,25 +94,31 @@ def result_headers(cl):
95 94
             if field_name == 'action_checkbox':
96 95
                 yield {
97 96
                     "text": text,
98  
-                    "class_attrib": mark_safe(' class="action-checkbox-column"')
  97
+                    "class_attrib": mark_safe(' class="action-checkbox-column"'),
  98
+                    "sortable": False,
99 99
                 }
100 100
                 continue
101 101
 
102 102
             admin_order_field = getattr(attr, "admin_order_field", None)
103 103
             if not admin_order_field:
104 104
                 # Not sortable
105  
-                yield {"text": text}
  105
+                yield {
  106
+                    "text": text,
  107
+                    "sortable": False,
  108
+                }
106 109
                 continue
107 110
 
108 111
         # OK, it is sortable if we got this far
109  
-        th_classes = []
  112
+        th_classes = ['sortable']
110 113
         order_type = ''
111 114
         new_order_type = 'asc'
112  
-        sort_pos = 0
  115
+        sort_priority = 0
  116
+        sorted = False
113 117
         # Is it currently being sorted on?
114 118
         if i in ordering_field_columns:
  119
+            sorted = True
115 120
             order_type = ordering_field_columns.get(i).lower()
116  
-            sort_pos = ordering_field_columns.keys().index(i) + 1
  121
+            sort_priority = ordering_field_columns.keys().index(i) + 1
117 122
             th_classes.append('sorted %sending' % order_type)
118 123
             new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
119 124
 
@@ -144,8 +149,9 @@ def result_headers(cl):
144 149
         yield {
145 150
             "text": text,
146 151
             "sortable": True,
  152
+            "sorted": sorted,
147 153
             "ascending": order_type == "asc",
148  
-            "sort_pos": sort_pos,
  154
+            "sort_priority": sort_priority,
149 155
             "url_primary": cl.get_query_string({ORDER_VAR: '.'.join(o_list_primary)}),
150 156
             "url_remove": cl.get_query_string({ORDER_VAR: '.'.join(o_list_remove)}),
151 157
             "url_toggle": cl.get_query_string({ORDER_VAR: '.'.join(o_list_toggle)}),
@@ -260,13 +266,14 @@ def result_list(cl):
260 266
     Displays the headers and data list together
261 267
     """
262 268
     headers = list(result_headers(cl))
  269
+    num_sorted_fields = 0
263 270
     for h in headers:
264  
-        # Sorting in templates depends on sort_pos attribute
265  
-        h.setdefault('sort_pos', 0)
  271
+        if h['sortable'] and h['sorted']:
  272
+            num_sorted_fields += 1
266 273
     return {'cl': cl,
267 274
             'result_hidden_fields': list(result_hidden_fields(cl)),
268 275
             'result_headers': headers,
269  
-            'reset_sorting_url': cl.get_query_string(remove=[ORDER_VAR]),
  276
+            'num_sorted_fields': num_sorted_fields,
270 277
             'results': list(results(cl))}
271 278
 
272 279
 @register.inclusion_tag('admin/date_hierarchy.html')
13  docs/releases/1.4.txt
@@ -360,13 +360,16 @@ Removed admin icons
360 360
 ~~~~~~~~~~~~~~~~~~~
361 361
 
362 362
 As part of an effort to improve the performance and usability of the admin's
363  
-vertical and horizontal "filter" widgets, some icon files were removed and
364  
-grouped into a single sprite file (``selector-icons.gif``):
  363
+changelist sorting interface and of the admin's :attr:`horizontal
  364
+<django.contrib.admin.ModelAdmin.filter_horizontal>` and :attr:`vertical
  365
+<django.contrib.admin.ModelAdmin.filter_vertical>` "filter" widgets, some icon
  366
+files were removed and grouped into two sprite files, respectively:
365 367
 ``selector-add.gif``, ``selector-addall.gif``, ``selector-remove.gif``,
366 368
 ``selector-removeall.gif``, ``selector_stacked-add.gif`` and
367  
-``selector_stacked-remove.gif``. If you used those icons to customize the
368  
-admin then you will want to replace them with your own icons or retrieve them
369  
-from a previous release.
  369
+``selector_stacked-remove.gif`` into ``selector-icons.gif``; and
  370
+``arrow-up.gif`` and ``arrow-down.gif`` into ``sorting-icons.gif``. If you used
  371
+those icons to customize the admin then you will want to replace them with your
  372
+own icons or retrieve them from a previous release.
370 373
 
371 374
 Compatibility with old signed data
372 375
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

0 notes on commit f2ed107

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