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
This commit is contained in:
Julien Phalip 2011-09-24 06:17:53 +00:00
parent b1c3174cfa
commit f2ed107b07
9 changed files with 104 additions and 186 deletions

View File

@ -309,77 +309,84 @@ tr.alt {
/* SORTABLE TABLES */ /* SORTABLE TABLES */
thead th {
padding: 0;
line-height: normal;
}
thead th a:link, thead th a:visited { thead th a:link, thead th a:visited {
color: #666; color: #666;
display: block;
} }
thead th.sorted { thead th.sorted {
background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x; background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
} }
table thead th.sorted a { table thead th .text span {
padding-right: 13px; padding: 2px 5px;
display:block;
} }
table thead th.ascending a { table thead th .text a {
background: url(../img/arrow-up.gif) right .4em no-repeat;
}
table thead th.descending a {
background: url(../img/arrow-down.gif) right .4em no-repeat;
}
table thead th.sorted a span.text {
display: block;
float: left;
cursor: pointer; /* IE needs this */
}
table thead th.sorted a span.sortpos {
display: block;
float: right;
font-size: .6em;
text-align: right;
cursor: pointer; /* IE needs this */
}
table thead th.sorted a img {
vertical-align: top;
}
table thead th.sorted a span.clear {
display: block;
clear: both;
}
#sorting-popup-div {
display: none;
position: absolute;
background-color: white;
border: 1px solid #ddd;
z-index: 2000; /* more than filters on right */
}
#sorting-popup-div table {
border-right: 0px;
border-left: 0px;
}
#sorting-popup-div .reset {
text-align: center;
}
#sorting-popup-div .cancel {
font-size: 10px;
background: #e1e1e1 url(../img/nav-bg.gif) 0 50% repeat-x;
border-top: 1px solid #ddd;
text-align: center;
}
#sorting-popup-div .cancel a {
width: 100%;
display: block; display: block;
cursor: pointer;
padding: 2px 5px;
}
table thead th.sortable:hover {
background: white url(../img/nav-bg-reverse.gif) 0 -5px repeat-x;
}
thead th.sorted a.sortremove {
visibility: hidden;
}
table thead th.sorted:hover a.sortremove {
visibility: visible;
}
table thead th.sorted .sortoptions {
display: block;
padding: 4px 5px 0 5px;
float: right;
text-align: right;
}
table thead th.sorted .sortpriority {
font-size: .8em;
min-width: 12px;
text-align: center;
vertical-align: top;
}
table thead th.sorted .sortoptions a {
width: 14px;
height: 12px;
display: inline-block;
}
table thead th.sorted .sortoptions a.sortremove {
background: url(../img/sorting-icons.gif) -4px -5px no-repeat;
}
table thead th.sorted .sortoptions a.sortremove:hover {
background: url(../img/sorting-icons.gif) -4px -27px no-repeat;
}
table thead th.sorted .sortoptions a.ascending {
background: url(../img/sorting-icons.gif) -5px -50px no-repeat;
}
table thead th.sorted .sortoptions a.ascending:hover {
background: url(../img/sorting-icons.gif) -5px -72px no-repeat;
}
table thead th.sorted .sortoptions a.descending {
background: url(../img/sorting-icons.gif) -5px -94px no-repeat;
}
table thead th.sorted .sortoptions a.descending:hover {
background: url(../img/sorting-icons.gif) -5px -115px no-repeat;
} }
/* ORDERABLE TABLES */ /* ORDERABLE TABLES */

View File

@ -80,22 +80,7 @@ div.breadcrumbs {
/* SORTABLE TABLES */ /* SORTABLE TABLES */
table thead th.sorted .sortoptions {
table thead th.sorted a {
padding-left: 13px;
padding-right: 0px;
}
table thead th.ascending a,
table thead th.descending a {
background-position: left;
}
table thead th.sorted a span.text {
float: right;
}
table thead th.sorted a span.sortpos {
float: left; float: left;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

View File

@ -11,15 +11,17 @@
<tr> <tr>
{% for header in result_headers %} {% for header in result_headers %}
<th scope="col" {{ header.class_attrib }}> <th scope="col" {{ header.class_attrib }}>
{% if header.sortable %}<a href="{{ header.url_primary }}">{% endif %} {% if header.sortable %}
<span class="text">{{ header.text|capfirst }}</span> {% if header.sort_priority > 0 %}
{% if header.sortable %} <div class="sortoptions">
{% if header.sort_pos > 0 %}<span class="sortpos"> <a class="sortremove" href="{{ header.url_remove }}" title="{% trans "Remove from sorting" %}"></a>
{% if header.sort_pos == 1 %}<img id="primary-sort-icon" src="{% static "admin/img/icon_cog.gif" %}" alt="" />&nbsp;{% endif %} {% 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 %}
{{ header.sort_pos }}</span> <a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% trans "Toggle sorting" %}"></a>
{% endif %} </div>
<span class="clear"></span></a> {% endif %}
{% endif %} {% endif %}
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
<div class="clear"></div>
</th>{% endfor %} </th>{% endfor %}
</tr> </tr>
</thead> </thead>
@ -33,90 +35,4 @@
</tbody> </tbody>
</table> </table>
</div> </div>
{# Sorting popup: #}
<div id="sorting-popup-div">
<table>
<caption>
{% trans "Sorting by:" %}
</caption>
<tbody>
{% for header in result_headers|dictsort:"sort_pos" %}
{% if header.sort_pos > 0 %}
<tr>
<td>{{ header.sort_pos }}</td>
<td>{{ header.text|capfirst }}</td>
<td>{% if header.ascending %}{% trans "ascending" %}{% else %}{% trans "descending" %}{% endif %}</td>
<td><a href="{{ header.url_toggle }}">{% trans "toggle" %}</a></td>
<td><a href="{{ header.url_remove }}">{% trans "remove" %}</a></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
<div class="reset"><a href="{{ reset_sorting_url }}">{% trans "Reset sorting" %}</a></div>
<div class="cancel"><a href="javascript:void" id="sorting-popup-dismiss">{% trans "Cancel" %}</a></div>
</div>
<script type="text/javascript">
<!--
(function($) {
$(document).ready(function() {
var popup = $('#sorting-popup-div');
var img = $('#primary-sort-icon');
/* These next lines seems necessary to prime the popup: */
popup.offset({left:0, top:-1000});
popup.show();
if ($.browser.msie) {
// Can't find a way to make IE autosize the div.
popup.width(300);
}
var popupWidth = popup.width();
popup.hide();
var visible = false;
var escHandler = function(ev) {
if (ev.which == 27) {
hidePopup();
ev.preventDefault();
}
};
var showPopup = function() {
var pos = img.offset();
pos.top += img.height();
if (pos.left + popupWidth >
$(window).width()) {
pos.left -= popupWidth;
}
popup.show();
popup.offset(pos);
visible = true;
$(document).bind('keyup', escHandler);
};
var hidePopup = function() {
popup.hide();
visible = false;
$(document).unbind('keyup', escHandler);
};
$('#primary-sort-icon').click(function(ev) {
ev.preventDefault();
if (visible) {
hidePopup();
} else {
showPopup();
}
});
$('#sorting-popup-dismiss').click(function(ev) {
hidePopup();
ev.preventDefault()
});
});
})(django.jQuery);
//-->
</script>
{% endif %} {% endif %}

View File

@ -83,7 +83,6 @@ def result_headers(cl):
""" """
ordering_field_columns = cl.get_ordering_field_columns() ordering_field_columns = cl.get_ordering_field_columns()
for i, field_name in enumerate(cl.list_display): for i, field_name in enumerate(cl.list_display):
admin_order_field = None
text, attr = label_for_field(field_name, cl.model, text, attr = label_for_field(field_name, cl.model,
model_admin = cl.model_admin, model_admin = cl.model_admin,
return_attr = True return_attr = True
@ -95,25 +94,31 @@ def result_headers(cl):
if field_name == 'action_checkbox': if field_name == 'action_checkbox':
yield { yield {
"text": text, "text": text,
"class_attrib": mark_safe(' class="action-checkbox-column"') "class_attrib": mark_safe(' class="action-checkbox-column"'),
"sortable": False,
} }
continue continue
admin_order_field = getattr(attr, "admin_order_field", None) admin_order_field = getattr(attr, "admin_order_field", None)
if not admin_order_field: if not admin_order_field:
# Not sortable # Not sortable
yield {"text": text} yield {
"text": text,
"sortable": False,
}
continue continue
# OK, it is sortable if we got this far # OK, it is sortable if we got this far
th_classes = [] th_classes = ['sortable']
order_type = '' order_type = ''
new_order_type = 'asc' new_order_type = 'asc'
sort_pos = 0 sort_priority = 0
sorted = False
# Is it currently being sorted on? # Is it currently being sorted on?
if i in ordering_field_columns: if i in ordering_field_columns:
sorted = True
order_type = ordering_field_columns.get(i).lower() order_type = ordering_field_columns.get(i).lower()
sort_pos = ordering_field_columns.keys().index(i) + 1 sort_priority = ordering_field_columns.keys().index(i) + 1
th_classes.append('sorted %sending' % order_type) th_classes.append('sorted %sending' % order_type)
new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type] new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
@ -144,8 +149,9 @@ def result_headers(cl):
yield { yield {
"text": text, "text": text,
"sortable": True, "sortable": True,
"sorted": sorted,
"ascending": order_type == "asc", "ascending": order_type == "asc",
"sort_pos": sort_pos, "sort_priority": sort_priority,
"url_primary": cl.get_query_string({ORDER_VAR: '.'.join(o_list_primary)}), "url_primary": cl.get_query_string({ORDER_VAR: '.'.join(o_list_primary)}),
"url_remove": cl.get_query_string({ORDER_VAR: '.'.join(o_list_remove)}), "url_remove": cl.get_query_string({ORDER_VAR: '.'.join(o_list_remove)}),
"url_toggle": cl.get_query_string({ORDER_VAR: '.'.join(o_list_toggle)}), "url_toggle": cl.get_query_string({ORDER_VAR: '.'.join(o_list_toggle)}),
@ -260,13 +266,14 @@ def result_list(cl):
Displays the headers and data list together Displays the headers and data list together
""" """
headers = list(result_headers(cl)) headers = list(result_headers(cl))
num_sorted_fields = 0
for h in headers: for h in headers:
# Sorting in templates depends on sort_pos attribute if h['sortable'] and h['sorted']:
h.setdefault('sort_pos', 0) num_sorted_fields += 1
return {'cl': cl, return {'cl': cl,
'result_hidden_fields': list(result_hidden_fields(cl)), 'result_hidden_fields': list(result_hidden_fields(cl)),
'result_headers': headers, 'result_headers': headers,
'reset_sorting_url': cl.get_query_string(remove=[ORDER_VAR]), 'num_sorted_fields': num_sorted_fields,
'results': list(results(cl))} 'results': list(results(cl))}
@register.inclusion_tag('admin/date_hierarchy.html') @register.inclusion_tag('admin/date_hierarchy.html')

View File

@ -360,13 +360,16 @@ Removed admin icons
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
As part of an effort to improve the performance and usability of the admin's 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 changelist sorting interface and of the admin's :attr:`horizontal
grouped into a single sprite file (``selector-icons.gif``): <django.contrib.admin.ModelAdmin.filter_horizontal>` and :attr:`vertical
<django.contrib.admin.ModelAdmin.filter_vertical>` "filter" widgets, some icon
files were removed and grouped into two sprite files, respectively:
``selector-add.gif``, ``selector-addall.gif``, ``selector-remove.gif``, ``selector-add.gif``, ``selector-addall.gif``, ``selector-remove.gif``,
``selector-removeall.gif``, ``selector_stacked-add.gif`` and ``selector-removeall.gif``, ``selector_stacked-add.gif`` and
``selector_stacked-remove.gif``. If you used those icons to customize the ``selector_stacked-remove.gif`` into ``selector-icons.gif``; and
admin then you will want to replace them with your own icons or retrieve them ``arrow-up.gif`` and ``arrow-down.gif`` into ``sorting-icons.gif``. If you used
from a previous release. 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~