Review Board

beta

Intelligently toggle column visibility and reload only the datagrids

Updated 11 months, 2 weeks ago

Christian Hammond Reviewers
trunk reviewboard
None Navi
We were taking the easy way out before with datagrid column customization and just pre-generating the toggle URLs up-front. When toggling the visibility of a column, we would change the URL. However, this would be problematic if you were then to rearrange columns and reload the page, as the original order would still be in the URL, causing your custom order to be wiped out.

We now smooth out the whole column customization process by making this all dynamic. Clicking a column in the menu generates the new column string based on the existing order in the grid and then saves the result to the server, requesting the new grid. We then unregister the old grid, load in the new one and register that.

The result is no more column specs in the URL, so no more wiping out of orders. The experience feels smoother, since we only reload the grid, rather than the whole page.

*UPDATE*

The latest change works in IE, but changes a bit how datagrids work, so it's worth looking at. I recommend viewing the interdiff as it's going to be a lot more clear.

IE refuses to load script tags when setting innerHTML. We were using the script tags to register the data grid and set up the columns. Now we scan for the grids and columns and set them up. This forces more requirements on what the HTML must be like when using the JavaScript, but I think that's fine.
Tested that I could add and remove columns as much as I wanted and still reorder things without losing any settings at all.

Tested in Firefox and IE.

Diff revision 6 (Latest)

5 6
5 6

  1. /trunk/djblets/djblets/datagrid/grids.py: 4 changes [ 1 2 3 4 ]
  2. /trunk/djblets/djblets/datagrid/templates/datagrid/listview.html: 5 changes [ 1 2 3 4 5 ]
  3. /trunk/djblets/djblets/media/js/datagrid.js: 23 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ]
/trunk/djblets/djblets/datagrid/grids.py
Revision 11688 New Change
1
from django.core.exceptions import ObjectDoesNotExist
1
from django.core.exceptions import ObjectDoesNotExist
2
from django.core.paginator import InvalidPage, ObjectPaginator
2
from django.core.paginator import InvalidPage, ObjectPaginator
3
from django.http import Http404, HttpResponse
3
from django.http import Http404, HttpResponse
4
from django.shortcuts import render_to_response
4
from django.shortcuts import render_to_response
5
from django.template.context import RequestContext
5
from django.template.context import RequestContext
6
from django.template.defaultfilters import date, timesince
6
from django.template.defaultfilters import date, timesince
7
from django.template.loader import render_to_string
7
from django.template.loader import render_to_string
8
from django.utils.safestring import mark_safe
8
from django.utils.safestring import mark_safe
9
from django.utils.translation import ugettext as _
9
from django.utils.translation import ugettext as _
10
from django.views.decorators.cache import cache_control
10
11
11
12
12
class Column(object):
13
class Column(object):
13
    """
14
    """
14
    A column in a data grid.
15
    A column in a data grid.
430
431
431
            if self.profile_sort_field and sort_str != profile_sort_list:
432
            if self.profile_sort_field and sort_str != profile_sort_list:
432
                setattr(profile, self.profile_sort_field, sort_str)
433
                setattr(profile, self.profile_sort_field, sort_str)
433
                profile_dirty = True
434
                profile_dirty = True
434
435
436
            print "Saving profile if... %s" % profile_dirty
435
            if profile_dirty:
437
            if profile_dirty:
436
                profile.save()
438
                profile.save()
437
439
438
        self.state_loaded = True
440
        self.state_loaded = True
439
441
537
                'pages': self.paginator.pages,
539
                'pages': self.paginator.pages,
538
                'hits': self.paginator.hits,
540
                'hits': self.paginator.hits,
539
                'page_range': self.paginator.page_range,
541
                'page_range': self.paginator.page_range,
540
            })))
542
            })))
541
543
544
    @cache_control(no_cache=True, no_store=True, max_age=0,
545
                   must_revalidate=True)
546
    def render_listview_to_response(self):
547
        """
548
        Renders the listview to a response, preventing caching in the
549
        process.
550
        """
551
        return HttpResponse(unicode(self.render_listview()))
552
542
    def render_to_response(self, template_name, extra_context={}):
553
    def render_to_response(self, template_name, extra_context={}):
543
        """
554
        """
544
        Renders a template containing this datagrid as a context variable.
555
        Renders a template containing this datagrid as a context variable.
545
        """
556
        """
546
        self.load_state()
557
        self.load_state()
547
558
548
        # If the caller is requesting just this particular grid, return it.
559
        # If the caller is requesting just this particular grid, return it.
549
        if self.request.GET.get('gridonly', False) and \
560
        if self.request.GET.get('gridonly', False) and \
550
           self.request.GET.get('datagrid-id', None) == self.id:
561
           self.request.GET.get('datagrid-id', None) == self.id:
551
            return HttpResponse(unicode(self.render_listview()))
562
            return self.render_listview_to_response()
552
563
553
        context = {
564
        context = {
554
            'datagrid': self
565
            'datagrid': self
555
        }
566
        }
556
        context.update(extra_context)
567
        context.update(extra_context)
/trunk/djblets/djblets/datagrid/templates/datagrid/listview.html
Revision 11688 New Change
1
{% load datagrid %}
1
{% load datagrid %}
2
<div class="datagrid-wrapper" id="{{datagrid.id}}">
2
<div class="datagrid-wrapper" id="{{datagrid.id}}">
3
 <div class="datagrid-titlebox">
3
 <div class="datagrid-titlebox">
4
{% block datagrid_title %}
4
{% block datagrid_title %}
5
   <h1 class="datagrid-title">{{datagrid.title}}</h1>
5
   <h1 class="datagrid-title">{{datagrid.title}}</h1>
6
{% endblock %}
6
{% endblock %}
7
 </div>
7
 </div>
8
 <div class="datagrid-main">
8
 <div class="datagrid-main">
9
  <table class="datagrid">
9
  <table class="datagrid">
10
   <colgroup>
10
   <colgroup>
11
{% for column in datagrid.columns %}
11
{% for column in datagrid.columns %}
12
    <col class="{{column.field_name}}"{% ifnotequal column.width 0 %} width="{{column.width}}%"{% endifnotequal %} />
12
    <col class="{{column.id}}"{% ifnotequal column.width 0 %} width="{{column.width}}%"{% endifnotequal %} />
13
{% endfor %}
13
{% endfor %}
14
    <col class="datagrid-customize" />
14
    <col class="datagrid-customize" />
15
   </colgroup>
15
   </colgroup>
16
   <thead>
16
   <thead>
17
    <tr class="datagrid-headers">
17
    <tr class="datagrid-headers">
31
  </table>
31
  </table>
32
{% if is_paginated %}
32
{% if is_paginated %}
33
{%  paginator %}
33
{%  paginator %}
34
{% endif %}
34
{% endif %}
35
 </div>
35
 </div>
36
</div>
37
<table class="datagrid-menu" id="{{datagrid.id}}-menu" style="visibility:hidden;position:absolute;top:0px;left:-1000px;">
36
 <table class="datagrid-menu" id="{{datagrid.id}}-menu" style="visibility:hidden;position:absolute;top:0px;left:-1000px;">
38
{% for column in datagrid.all_columns %}
37
{% for column in datagrid.all_columns %}
39
{%  with column.toggle_url as toggle_url %}
38
{%  with column.toggle_url as toggle_url %}
40
 <tr>
39
  <tr>
41
  <td><div class="datagrid-menu-checkbox" onclick="javascript:window.location='{{toggle_url}}';">{% if column.active %}<img src="{{MEDIA_URL}}images/djblets/datagrid/checkmark.png" width="8" height="8" border="0" alt="X" />{% endif %}</div></td>
40
   <td><div class="datagrid-menu-checkbox" onclick="DJBLETS.datagrids.toggleColumn('{{datagrid.id}}', '{{column.id}}');">{% if column.active %}<img src="{{MEDIA_URL}}images/djblets/datagrid/checkmark.png" width="8" height="8" border="0" alt="X" />{% endif %}</div></td>
42
  <td class="datagrid-menu-label"><a href=".{{toggle_url}}">
41
   <td class="datagrid-menu-label"><a href="#" onclick="DJBLETS.datagrids.toggleColumn('{{datagrid.id}}', '{{column.id}}');">
43
{%   if column.image_url %}
42
{%   if column.image_url %}
44
   <img src="{{column.image_url}}" width="{{column.image_width}}" height="{{column.image_height}}" alt="{{column.image_alt}}" />
43
    <img src="{{column.image_url}}" width="{{column.image_width}}" height="{{column.image_height}}" alt="{{column.image_alt}}" />
45
{%   endif %}
44
{%   endif %}
46
   {{column.detailed_label|default_if_none:""}}</a>
45
    {{column.detailed_label|default_if_none:""}}</a>
47
{%  endwith %}
46
{%  endwith %}
48
{% endfor %}
47
{% endfor %}
49
</table>
48
 </table>
50
<script type="text/javascript">
49
</div>
51
DJBLETS.datagrids.registerDataGrid("{{datagrid.id}}", [
52
{% for column in datagrid.columns %}"{{column.id}}"{% if not forloop.last %}, {%endif %}{% endfor %}
53
]);
54
</script>
/trunk/djblets/djblets/media/js/datagrid.js
Revision 11688 New Change
1
/* Create the DJBLETS namespace if it doesn't exist. */
1
/* Create the DJBLETS namespace if it doesn't exist. */
2
if (!DJBLETS) {
2
if (!DJBLETS) {
3
	var DJBLETS = {};
3
    var DJBLETS = {};
4
}
4
}
5
5
6
DJBLETS.datagrids = {
6
DJBLETS.datagrids = {
7
	activeMenu: null,
7
    activeMenu: null,
8
	registeredGrids: [],
9
	activeColumns: {},
8
    activeColumns: {},
10
9
11
	/*
10
    /*
12
	 * Registers a datagrid. This will cause drag and drop and column
11
     * Registers a datagrid. This will cause drag and drop and column
13
	 * customization to be enabled.
12
     * customization to be enabled.
14
	 *
13
     *
15
	 * @param {string} datagrid_id    The ID of the datagrid.
14
     * @param {HTMLElement} grid  The datagrid element.
16
	 * @param {array}  activeColumns  The list of active columns in order.
17
	 */
15
     */
18
	registerDataGrid: function(datagrid_id, activeColumns) {
16
    registerDataGrid: function(grid) {
19
		this.registeredGrids.push(datagrid_id);
17
        this.activeColumns[grid] = [];
20
		this.activeColumns[datagrid_id] = activeColumns;
18
19
        var cols = grid.getElementsByTagName("col");
20
        for (var j = 0; j < cols.length; j++) {
21
            if (cols[j].className != "datagrid-customize") {
22
                this.activeColumns[grid].push(cols[j].className);
23
            }
24
        }
25
26
        var headers = grid.getElementsByTagName("th");
27
28
        for (var j = 0; j < headers.length; j++) {
29
            var header = getEl(headers[j]);
30
            header.unselectable();
31
32
            if (!header.hasClass("edit-columns")) {
33
                new DJBLETS.datagrids.DDColumn(header, grid);
34
            }
35
        }
36
    },
37
38
    /*
39
     * Unregisters a datagrid. This is used when we're getting ready to
40
     * reload a grid.
41
     *
42
     * @param {HTMLElement} grid  The datagrid.
43
     */
44
    unregisterDataGrid: function(grid) {
45
        this.activeColumns[grid] = [];
21
	},
46
    },
22
47
23
	/*
48
    /*
24
	 * Hides the currently open columns menu.
49
     * Hides the currently open columns menu.
25
	 */
50
     */
26
	hideColumnsMenu: function() {
51
    hideColumnsMenu: function() {
52
        if (this.activeMenu != null) {
27
		this.activeMenu.hide();
53
            this.activeMenu.hide();
28
		this.activeMenu = null;
54
            this.activeMenu = null;
55
        }
29
	},
56
    },
30
57
31
	/*
58
    /*
32
	 * Toggles the visibility of the specified columns menu.
59
     * Toggles the visibility of the specified columns menu.
33
	 *
60
     *
56
			YAHOO.util.Event.stopEvent(evt);
83
            YAHOO.util.Event.stopEvent(evt);
57
		}
84
        }
58
	},
85
    },
59
86
60
	/*
87
    /*
61
	 * Callback handler for when the page finishes loading. Enables
88
     * Callback handler for when the page finishes loading. Registers
62
	 * drag and drop for the datagrids.
89
     * the grids and enables drag and drop for the datagrids.
63
	 */
90
     */
64
	onPageLoad: function() {
91
    onPageLoad: function() {
65
		for (var i = 0; i < this.registeredGrids.length; i++) {
92
        var grids = YAHOO.util.Dom.getElementsByClassName("datagrid-wrapper",
66
			var grid = getEl(this.registeredGrids[i]);
93
                                                          "div");
67
			var headers = grid.getChildrenByTagName("th");
94
        for (var i = 0; i < grids.length; i++) {
95
            this.registerDataGrid(grids[i]);
96
        }
97
    },
68
98
69
			for (var j = 0; j < headers.length; j++) {
99
    /*
70
				headers[j].unselectable();
100
     * Saves the new columns list on the server.
101
     *
102
     * @param {{string}}   gridId      The ID of the datagrid.
103
     * @param {{string}}   columnsStr  The columns to display.
104
     * @param {{function}} onSuccess   Optional callback on successful save.
105
     */
106
    saveColumns: function(gridId, columnsStr, onSuccess) {
107
        var url = window.location.pathname +
108
                  "?gridonly=1&datagrid-id=" + gridId +
109
                  "&columns=" + columnsStr;
110
111
        YAHOO.util.Connect.asyncRequest("GET", url, {
112
            success: onSuccess
113
        });
114
    },
71
115
72
				if (!headers[j].hasClass("edit-columns")) {
116
    /*
73
					new DJBLETS.datagrids.DDColumn(headers[j], grid);
117
     * Toggles the visibility of a column. This will build the resulting
118
     * columns string and request a save of the columns, followed by a
119
     * reload of the page.
120
     *
121
     * @param {{string}}  gridId    The ID of the datagrid.
122
     * @param {{string}}  columnId  The ID of the column to toggle.
123
     */
124
    toggleColumn: function(gridId, columnId) {
125
        var addingColumn = true;
126
        var grid = document.getElementById(gridId);
127
        var curColumns = this.activeColumns[grid];
128
        var newColumnsStr = "";
129
130
        for (var i = 0; i < curColumns.length; i++) {
131
            if (curColumns[i] == columnId) {
132
                /* We're removing this column. */
133
                addingColumn = false;
134
            } else {
135
                newColumnsStr += curColumns[i];
136
137
                if (i < curColumns.length - 1) {
138
                    newColumnsStr += ",";
74
				}
139
                }
75
			}
140
            }
76
		}
141
        }
142
143
        if (addingColumn) {
144
            newColumnsStr += "," + columnId;
145
        }
146
147
        this.saveColumns(gridId, newColumnsStr, function(res) {
148
            this.hideColumnsMenu();
149
            this.unregisterDataGrid(gridId);
150
151
            /* The resulting text *should* be datagrid HTML. */
152
            var oldEl = getEl(gridId);
153
            oldEl.dom.id = "";
154
155
            YAHOO.ext.DomHelper.insertHtml("beforeBegin", oldEl.dom,
156
                                           res.responseText);
157
            oldEl.remove();
158
159
            this.registerDataGrid(document.getElementById(gridId));
160
        }.createDelegate(this));
77
	}
161
    }
78
}
162
}
79
163
80
164
81
/*
165
/*
119
	 * Sets up the movement constraints for this column. This locks the
203
     * Sets up the movement constraints for this column. This locks the
120
	 * column into the column header region. It has the effect of only
204
     * column into the column header region. It has the effect of only
121
	 * allowing the column to slide left and right.
205
     * allowing the column to slide left and right.
122
	 */
206
     */
123
	initConstraints: function() {
207
    initConstraints: function() {
124
		var thead = this.grid.getChildrenByTagName("thead")[0];
208
        var thead = getEl(this.grid.getElementsByTagName("thead")[0]);
125
		var headerRegion = thead.getRegion();
209
        var headerRegion = thead.getRegion();
126
		var colRegion = this.el.getRegion();
210
        var colRegion = this.el.getRegion();
127
211
128
		this.setXConstraint(colRegion.left - headerRegion.left,
212
        this.setXConstraint(colRegion.left - headerRegion.left,
129
		                    headerRegion.right - colRegion.right);
213
                            headerRegion.right - colRegion.right);
171
		dragEl.hide();
255
        dragEl.hide();
172
		this.el.show();
256
        this.el.show();
173
257
174
		this.columnMidpoints = [];
258
        this.columnMidpoints = [];
175
259
176
		this.saveColumns();
260
        /* Build the new columns list. */
261
        var columns = DJBLETS.datagrids.activeColumns[this.grid];
262
        var columnsStr = "";
263
264
        for (var i = 0; i < columns.length; i++) {
265
            columnsStr += columns[i];
266
267
            if (i != columns.length - 1) {
268
                columnsStr += ",";
269
            }
270
        }
271
272
        DJBLETS.datagrids.saveColumns(this.grid.id, columnsStr);
177
	},
273
    },
178
274
179
	/*
275
    /*
180
	 * Handles movement while in drag mode.
276
     * Handles movement while in drag mode.
181
	 *
277
     *
228
	 * of the currently dragged column.
324
     * of the currently dragged column.
229
	 */
325
     */
230
	buildColumnInfo: function() {
326
    buildColumnInfo: function() {
231
		/* Grab the list of midpoints for each column. */
327
        /* Grab the list of midpoints for each column. */
232
		this.columnMidpoints = [];
328
        this.columnMidpoints = [];
233
		var columns = this.grid.getChildrenByTagName("th");
329
        var columns = getEl(this.grid).getChildrenByTagName("th");
234
330
235
		for (var i = 0; i < columns.length; i++) {
331
        for (var i = 0; i < columns.length; i++) {
236
			if (!columns[i].hasClass("edit-columns")) {
332
            if (!columns[i].hasClass("edit-columns")) {
237
				this.columnMidpoints.push(columns[i].getX() +
333
                this.columnMidpoints.push(columns[i].getX() +
238
				                          columns[i].getWidth() / 2);
334
                                          columns[i].getWidth() / 2);
255
	 * @param {int} beforeIndex The index of the column to place the first
351
     * @param {int} beforeIndex The index of the column to place the first
256
	 *                          before.
352
     *                          before.
257
	 */
353
     */
258
	swapColumnBefore: function(index, beforeIndex) {
354
    swapColumnBefore: function(index, beforeIndex) {
259
		/* Swap the column info. */
355
        /* Swap the column info. */
260
		var colTags = this.grid.getChildrenByTagName("col");
356
        var colTags = getEl(this.grid).getChildrenByTagName("col");
261
		colTags[index].insertBefore(colTags[beforeIndex]);
357
        colTags[index].insertBefore(colTags[beforeIndex]);
262
358
263
		/* Swap the list of active columns */
359
        /* Swap the list of active columns */
264
		var tempName = DJBLETS.datagrids.activeColumns[this.grid.id][index];
360
        var tempName = DJBLETS.datagrids.activeColumns[this.grid][index];
265
		DJBLETS.datagrids.activeColumns[this.grid.id][index] =
361
        DJBLETS.datagrids.activeColumns[this.grid][index] =
266
			DJBLETS.datagrids.activeColumns[this.grid.id][beforeIndex];
362
            DJBLETS.datagrids.activeColumns[this.grid][beforeIndex];
267
		DJBLETS.datagrids.activeColumns[this.grid.id][beforeIndex] = tempName;
363
        DJBLETS.datagrids.activeColumns[this.grid][beforeIndex] = tempName;
268
364
269
		/* Swap the cells. This will include the headers. */
365
        /* Swap the cells. This will include the headers. */
270
		var table = this.grid.getChildrenByTagName("table")[0].dom;
366
        var table = getEl(this.grid).getChildrenByTagName("table")[0].dom;
271
		for (var i = 0; i < table.rows.length; i++) {
367
        for (var i = 0; i < table.rows.length; i++) {
272
			var row = table.rows[i];
368
            var row = table.rows[i];
273
			var cell = row.cells[index];
369
            var cell = row.cells[index];
274
			var beforeCell = row.cells[beforeIndex];
370
            var beforeCell = row.cells[beforeIndex];
275
371
281
			beforeCell.colSpan = tempColSpan;
377
            beforeCell.colSpan = tempColSpan;
282
		}
378
        }
283
379
284
		/* Everything has changed, so rebuild our view of things. */
380
        /* Everything has changed, so rebuild our view of things. */
285
		this.buildColumnInfo();
381
        this.buildColumnInfo();
286
	},
287
288
	/*
289
	 * Saves the new columns list on the server.
290
	 */
291
	saveColumns: function() {
292
		var columns = "";
293
		var grid_id = this.grid.id;
294
		var len = DJBLETS.datagrids.activeColumns[grid_id].length;
295
296
		for (var i = 0; i < len; i++) {
297
			columns += DJBLETS.datagrids.activeColumns[grid_id][i];
298
299
			if (i != len - 1) {
300
				columns += ",";
301
			}
302
		}
303
304
		var url = window.location.pathname +
305
		          "?gridonly=1&datagrid-id=" + grid_id + "&columns=" + columns;
306
307
		YAHOO.util.Connect.asyncRequest("GET", url);
308
	}
382
    }
309
});
383
});
310
384
311
YAHOO.util.Event.on(window, "load",
385
YAHOO.util.Event.on(window, "load",
312
                    DJBLETS.datagrids.onPageLoad.createDelegate(DJBLETS.datagrids));
386
                    DJBLETS.datagrids.onPageLoad.createDelegate(DJBLETS.datagrids));
313
YAHOO.util.Event.on(document, "click", function(e) {
387
YAHOO.util.Event.on(document, "click", function(e) {
314
	if (DJBLETS.<