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:
parent
b1c3174cfa
commit
f2ed107b07
|
@ -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 */
|
||||||
|
|
|
@ -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 |
|
@ -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="" /> {% 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 %}
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
Loading…
Reference in New Issue