diff --git a/Gruntfile.js b/Gruntfile.js index a74a97e7..9009de2f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -108,6 +108,7 @@ module.exports = function (grunt) { }, helpers: "vendor/js/jasmine-html.js", vendor: [ + "test/vendor/js/synthetic-dom-events.js", "test/vendor/js/jquery.js", "test/vendor/js/underscore.js", "test/vendor/js/backbone.js", diff --git a/categories.json b/categories.json index 4c2565fd..b352f198 100644 --- a/categories.json +++ b/categories.json @@ -52,6 +52,7 @@ }, { "name": "Others", "classes": [ + "Backgrid.View", "Backgrid.Command", "Backgrid.Grid", "Backgrid.Body", diff --git a/lib/backgrid.js b/lib/backgrid.js index 6f8da79e..7919cfcb 100644 --- a/lib/backgrid.js +++ b/lib/backgrid.js @@ -31,18 +31,18 @@ // Copyright 2009, 2010 Kristopher Michael Kowal // https://github.com/kriskowal/es5-shim // ES5 15.5.4.20 -// http://es5.github.com/#x15.5.4.20 +// whitespace from: http://es5.github.io/#x15.5.4.20 var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + - "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + - "\u2029\uFEFF"; + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; if (!String.prototype.trim || ws.trim()) { // http://blog.stevenlevithan.com/archives/faster-trim-javascript // http://perfectionkills.com/whitespace-deviations/ ws = "[" + ws + "]"; var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), - trimEndRegexp = new RegExp(ws + ws + "*$"); + trimEndRegexp = new RegExp(ws + ws + "*$"); String.prototype.trim = function trim() { - if (this === undefined || this === null) { + if (this === void 0 || this === null) { throw new TypeError("can't convert " + this + " to object"); } return String(this) @@ -61,8 +61,6 @@ function lpad(str, length, padstr) { return padding + str; } -var $ = Backbone.$; - var Backgrid = root.Backgrid = { Extension: {}, @@ -169,6 +167,228 @@ _.extend(Command.prototype, { } }); +// Caches a local reference to `Element.prototype` for faster access. +var ElementProto = typeof Element != 'undefined' && Element.prototype; + +// Given an object `obj` and an operation `op`, which must be either `add` or +// `remove`, this function returns either the native implementation of the +// [add|remove]EventListener method directly or a function that delegates to +// IE's [attach|detach]Event methods. +function makeEventListener(obj, op) { + if (obj[op + 'EventListener']) return obj[op + 'EventListener']; + var func = op == 'add' ? obj.attachEvent : obj.detachEvent; + return function (eventName, listener) { + func.call(this, 'on' + eventName, listener); + }; +} + +// Caches the Element prototype's event listener methods for everything else. +var elementAddEventListener = makeEventListener(ElementProto, 'add'); +var elementRemoveEventListener = makeEventListener(ElementProto, 'remove'); + +// Find the right `Element#matches` for IE>=9 and modern browsers. +var matchesSelector = ElementProto && ElementProto.matches || + ElementProto[_.find(['webkit', 'moz', 'ms', 'o'], function (prefix) { + return !!ElementProto[prefix + 'MatchesSelector']; + }) + 'MatchesSelector'] || + // Make our own `Element#matches` for IE8 + function (selector) { + // We'll use querySelectorAll to find all element matching the selector, + // then check if the given element is included in that list. + // Executing the query on the parentNode reduces the resulting nodeList, + // document doesn't have a parentNode, though. + var nodeList = (this.parentNode || document).querySelectorAll(selector) || []; + for (var i = 0, l = nodeList.length; i < l; i++) { + if (nodeList[i] == this) return true; + } + return false; + }; + +// Cached regex to split keys for `delegate`. +var delegateEventSplitter = /^(\S+)\s*(.*)$/; + +// Cached regex to match an opening '<' of an HTML tag, possibly left-padded +// with whitespace. +var paddedLt = /^\s*", { - tabIndex: -1, - href: rawValue, - title: this.title || formattedValue, - target: this.target - }).text(formattedValue)); + this.empty(); + var model = this.model; + var rawValue = model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, model); + var a = document.createElement("a"); + a.tabIndex = -1; + a.href = rawValue; + a.title = this.title || formattedValue; + a.target = this.target; + a.appendChild(document.createTextNode(formattedValue)); + this.el.appendChild(a); this.delegateEvents(); return this; } @@ -1043,14 +1272,16 @@ var EmailCell = Backgrid.EmailCell = StringCell.extend({ formatter: EmailFormatter, render: function () { - this.$el.empty(); + this.empty(); var model = this.model; - var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.append($("", { - tabIndex: -1, - href: "mailto:" + formattedValue, - title: formattedValue - }).text(formattedValue)); + var rawValue = model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, model); + var a = document.createElement("a"); + a.tabIndex = -1; + a.href = "mailto:" + rawValue; + a.title = formattedValue; + a.appendChild(document.createTextNode(formattedValue)); + this.el.appendChild(a); this.delegateEvents(); return this; } @@ -1290,7 +1521,7 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ render: function () { var model = this.model; var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.prop("checked", val); + this.el.defaultChecked = val; return this; }, @@ -1318,23 +1549,23 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ // skip ahead to `change` when space is pressed if (command.passThru() && e.type != "change") return true; if (command.cancel()) { - e.stopPropagation(); model.trigger("backgrid:edited", model, column, command); + e.stopPropagation(); } - var $el = this.$el; + var el = this.el; if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - var val = formatter.toRaw($el.prop("checked"), model); + var val = formatter.toRaw(el.checked, model); model.set(column.get("name"), val); model.trigger("backgrid:edited", model, column, command); + e.preventDefault(); + e.stopPropagation(); } else if (e.type == "change") { - var val = formatter.toRaw($el.prop("checked"), model); + var val = formatter.toRaw(el.checked, model); model.set(column.get("name"), val); - $el.focus(); + el.focus(); } } @@ -1365,15 +1596,15 @@ var BooleanCell = Backgrid.BooleanCell = Cell.extend({ uncheck otherwise. */ render: function () { - this.$el.empty(); + this.empty(); var model = this.model, column = this.column; var editable = Backgrid.callByNeed(column.editable(), column, model); - this.$el.append($("", { - tabIndex: -1, - type: "checkbox", - checked: this.formatter.fromRaw(model.get(column.get("name")), model), - disabled: !editable - })); + var input = document.createElement("input"); + input.tabIndex = -1; + input.type = "checkbox"; + input.checked = this.formatter.fromRaw(model.get(this.column.get("name")), model); + input.disabled = !editable; + this.el.appendChild(input); this.delegateEvents(); return this; } @@ -1398,9 +1629,6 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ "keydown": "close" }, - /** @property {function(Object, ?Object=): string} template */ - template: _.template('', null, {variable: null}), - setOptionValues: function (optionValues) { this.optionValues = optionValues; this.optionValues = _.result(this, "optionValues"); @@ -1408,17 +1636,21 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ setMultiple: function (multiple) { this.multiple = multiple; - this.$el.prop("multiple", multiple); + this.el.multiple = multiple; }, _renderOptions: function (nvps, selectedValues) { - var options = ''; + var options = []; for (var i = 0; i < nvps.length; i++) { - options = options + this.template({ - text: nvps[i][0], - value: nvps[i][1], - selected: _.indexOf(selectedValues, nvps[i][1]) > -1 - }); + var nvp = nvps[i]; + var name = nvps[i][0]; + var value = nvps[i][1]; + var option = document.createElement("option"); + option.value = value; + option.defaultSelected = _.indexOf(selectedValues, value) > -1; + option.selected = option.defaultSelected; + option.appendChild(document.createTextNode(name)); + options.push(option); } return options; }, @@ -1431,7 +1663,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ parameter. */ render: function () { - this.$el.empty(); + this.empty(); var optionValues = _.result(this, "optionValues"); var model = this.model; @@ -1441,34 +1673,42 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ var optionValue = null; var optionText = null; - var optionValue = null; var optgroupName = null; - var optgroup = null; + var children = []; - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; + for (var i = 0; i < optionValues.length; ++i) { + optionValue = optionValues[i]; if (_.isArray(optionValue)) { optionText = optionValue[0]; optionValue = optionValue[1]; - this.$el.append(this.template({ - text: optionText, - value: optionValue, - selected: _.indexOf(selectedValues, optionValue) > -1 - })); + var option = document.createElement("option"); + option.value = optionValue; + option.defaultSelected = _.indexOf(selectedValues, optionValue) > -1; + option.selected = option.defaultSelected; + option.appendChild(document.createTextNode(optionText)); + children.push(option); } else if (_.isObject(optionValue)) { optgroupName = optionValue.name; - optgroup = $("", { label: optgroupName }); - optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues)); - this.$el.append(optgroup); + var optgroup = document.createElement("optgroup"); + optgroup.label = optgroupName; + var options = this._renderOptions(optionValue.values, selectedValues); + for (var j = 0; j < options.length; j++) { + optgroup.appendChild(options[j]); + } + children.push(optgroup); } else { throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); } } + for (var i = 0; i < children.length; ++i) { + this.el.appendChild(children[i]); + } + this.delegateEvents(); return this; @@ -1480,7 +1720,20 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ save: function (e) { var model = this.model; var column = this.column; - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); + + var values = null; + var options = this.el.options, option; + if (!this.multiple) values = this.el.value || null; + else if (this.multiple) { + values = []; + for (var i = 0, l = options.length; i < l; i++) { + option = options[i]; + if (option.selected) values.push(option.value); + } + if (values.length === 0) values = null; + } + + model.set(column.get("name"), this.formatter.toRaw(values, model)); }, /** @@ -1492,15 +1745,15 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ var column = this.column; var command = new Command(e); if (command.cancel()) { - e.stopPropagation(); model.trigger("backgrid:edited", model, column, new Command(e)); + e.stopPropagation(); } else if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || command.moveDown() || e.type == "blur") { - e.preventDefault(); - e.stopPropagation(); this.save(e); model.trigger("backgrid:edited", model, column, new Command(e)); + e.preventDefault(); + e.stopPropagation(); } } @@ -1585,7 +1838,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ @throws {TypeError} If `optionValues` is malformed. */ render: function () { - this.$el.empty(); + this.empty(); var optionValues = _.result(this, "optionValues"); var model = this.model; @@ -1604,7 +1857,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ if (_.isArray(optionValue)) { var optionText = optionValue[0]; - var optionValue = optionValue[1]; + optionValue = optionValue[1]; if (optionValue == rawDatum) selectedText.push(optionText); } @@ -1624,7 +1877,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ } } - this.$el.append(selectedText.join(this.delimiter)); + this.el.appendChild(window.document.createTextNode(selectedText.join(this.delimiter))); } catch (ex) { if (ex instanceof TypeError) { @@ -1874,9 +2127,9 @@ var Columns = Backgrid.Columns = Backbone.Collection.extend({ rendered, and apply the appropriate cell to each attribute. @class Backgrid.Row - @extends Backbone.View + @extends Backgrid.View */ -var Row = Backgrid.Row = Backbone.View.extend({ +var Row = Backgrid.Row = Backgrid.View.extend({ /** @property */ tagName: "tr", @@ -1907,15 +2160,15 @@ var Row = Backgrid.Row = Backbone.View.extend({ var cell = this.makeCell(column, options); cells.splice(i, 0, cell); - var $el = this.$el; + var el = this.el, children = el.childNodes; if (i === 0) { - $el.prepend(cell.render().$el); + el.insertBefore(cell.render().el, el.firstChild); } else if (i === columns.length - 1) { - $el.append(cell.render().$el); + el.appendChild(cell.render().el); } else { - $el.children().eq(i).before(cell.render().$el); + el.insertBefore(cell.render().el, children[i]); } }); @@ -1947,7 +2200,7 @@ var Row = Backgrid.Row = Backbone.View.extend({ Renders a row of cells for this row's model. */ render: function () { - this.$el.empty(); + this.empty(); var fragment = document.createDocumentFragment(); for (var i = 0; i < this.cells.length; i++) { @@ -1971,7 +2224,7 @@ var Row = Backgrid.Row = Backbone.View.extend({ var cell = this.cells[i]; cell.remove.apply(cell, arguments); } - return Backbone.View.prototype.remove.apply(this, arguments); + return Row.__super__.remove.apply(this, arguments); } }); @@ -1981,9 +2234,9 @@ var Row = Backgrid.Row = Backbone.View.extend({ row with a single column. @class Backgrid.EmptyRow - @extends Backbone.View + @extends Backgrid.View */ -var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ +var EmptyRow = Backgrid.EmptyRow = Backgrid.View.extend({ /** @property */ tagName: "tr", @@ -2007,7 +2260,7 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ Renders an empty row. */ render: function () { - this.$el.empty(); + this.empty(); var td = document.createElement("td"); td.setAttribute("colspan", this.columns.length); @@ -2034,9 +2287,9 @@ var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ refresh after sorting. @class Backgrid.HeaderCell - @extends Backbone.View + @extends Backgrid.View */ -var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ +var HeaderCell = Backgrid.HeaderCell = Backgrid.View.extend({ /** @property */ tagName: "th", @@ -2060,23 +2313,27 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ this.column = new Column(this.column); } - var column = this.column, collection = this.collection, $el = this.$el; + var column = this.column + var collection = this.collection + var el = this.el; + var classes = el.classList; this.listenTo(column, "change:editable change:sortable change:renderable", function (column) { var changed = column.changedAttributes(); for (var key in changed) { if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); + if (changed[key]) classes.add(key); + else classes.remove(key); } } }); this.listenTo(column, "change:direction", this.setCellDirection); this.listenTo(column, "change:name change:label", this.render); - if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable"); - if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable"); - if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable"); + if (Backgrid.callByNeed(column.editable(), column, collection)) classes.add("editable"); + if (Backgrid.callByNeed(column.sortable(), column, collection)) classes.add("sortable"); + if (Backgrid.callByNeed(column.renderable(), column, collection)) classes.add("renderable"); this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection); }, @@ -2086,7 +2343,9 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ direction classes. */ removeCellDirection: function () { - this.$el.removeClass("ascending").removeClass("descending"); + var classes = this.el.classList; + classes.remove("ascending"); + classes.remove("descending"); this.column.set("direction", null); }, @@ -2097,8 +2356,10 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ otherwise. */ setCellDirection: function (column, direction) { - this.$el.removeClass("ascending").removeClass("descending"); - if (column.cid == this.column.cid) this.$el.addClass(direction); + var classes = this.el.classList; + classes.remove("ascending"); + classes.remove("descending"); + if (column.cid == this.column.cid) classes.add(direction); }, /** @@ -2107,8 +2368,6 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ `ascending`, `descending`, and default. */ onClick: function (e) { - e.preventDefault(); - var column = this.column; var collection = this.collection; var event = "backgrid:sort"; @@ -2130,6 +2389,8 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ if (sortType === "toggle") toggleSort(this, column); else cycleSort(this, column); } + + e.preventDefault(); }, /** @@ -2137,19 +2398,29 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ column. */ render: function () { - this.$el.empty(); + this.empty(); + + var label; + var column = this.column; var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - var label; - if(sortable){ - label = $("").text(column.get("label")).append(""); - } else { - label = document.createTextNode(column.get("label")); + + if (sortable) { + label = document.createElement("a"); + label.appendChild(document.createTextNode(this.column.get("label"))); + var caret = document.createElement("b"); + caret.className = "sort-caret"; + label.appendChild(caret); } + else label = document.createTextNode(column.get("label")); + this.el.appendChild(label); + + var classes = this.el.classList; + classes.add(column.get("name")); + + var direction = column.get("direction"); + if (direction) classes.add(direction); - this.$el.append(label); - this.$el.addClass(column.get("name")); - this.$el.addClass(column.get("direction")); this.delegateEvents(); return this; } @@ -2179,7 +2450,7 @@ var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ @throws {TypeError} If options.columns or options.collection is undefined. */ initialize: function () { - Backgrid.Row.prototype.initialize.apply(this, arguments); + HeaderRow.__super__.initialize.apply(this, arguments); }, makeCell: function (column, options) { @@ -2198,9 +2469,9 @@ var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ single row of header cells. @class Backgrid.Header - @extends Backbone.View + @extends Backgrid.View */ -var Header = Backgrid.Header = Backbone.View.extend({ +var Header = Backgrid.Header = Backgrid.View.extend({ /** @property */ tagName: "thead", @@ -2231,7 +2502,7 @@ var Header = Backgrid.Header = Backbone.View.extend({ Renders this table head with a single row of header cells. */ render: function () { - this.$el.append(this.row.render().$el); + this.el.appendChild(this.row.render().el); this.delegateEvents(); return this; }, @@ -2243,7 +2514,7 @@ var Header = Backgrid.Header = Backbone.View.extend({ */ remove: function () { this.row.remove.apply(this.row, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); + return Header.__super__.remove.apply(this, arguments); } }); @@ -2261,9 +2532,9 @@ var Header = Backgrid.Header = Backbone.View.extend({ responsible for refreshing the rows after sorting, insertion and removal. @class Backgrid.Body - @extends Backbone.View + @extends Backgrid.View */ -var Body = Backgrid.Body = Backbone.View.extend({ +var Body = Backgrid.Body = Backgrid.View.extend({ /** @property */ tagName: "tbody", @@ -2361,16 +2632,12 @@ var Body = Backgrid.Body = Backbone.View.extend({ var index = collection.indexOf(model); this.rows.splice(index, 0, row); - var $el = this.$el; - var $children = $el.children(); - var $rowEl = row.render().$el; + var el = this.el; + var children = el.childNodes; + var rowEl = row.render().el; - if (index >= $children.length) { - $el.append($rowEl); - } - else { - $children.eq(index).before($rowEl); - } + if (index >= children.length) el.appendChild(rowEl); + else el.insertBefore(rowEl, children[index]); return this; }, @@ -2424,9 +2691,16 @@ var Body = Backgrid.Body = Backbone.View.extend({ instance as its sole parameter when done. */ refresh: function () { - for (var i = 0; i < this.rows.length; i++) { - this.rows[i].remove(); - } + var parent = this.el.parentNode; + if (parent) parent.removeChild(this.el); + + // GC the damn rows in the background + var oldRows = [].slice.apply(this.rows); + setTimeout(function () { + for (var i = 0; i < oldRows.length; i++) { + oldRows[i].remove(); + } + }, 0); this.rows = this.collection.map(function (model) { var row = new this.row({ @@ -2436,10 +2710,13 @@ var Body = Backgrid.Body = Backbone.View.extend({ return row; }, this); + this._unshiftEmptyRowMayBe(); this.render(); + if (parent) parent.appendChild(this.el); + this.collection.trigger("backgrid:refresh", this); return this; @@ -2451,7 +2728,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ row is rendered, otherwise no row is rendered. */ render: function () { - this.$el.empty(); + this.empty(); var fragment = document.createDocumentFragment(); for (var i = 0; i < this.rows.length; i++) { @@ -2476,7 +2753,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ var row = this.rows[i]; row.remove.apply(row, arguments); } - return Backbone.View.prototype.remove.apply(this, arguments); + return Body.__super__.remove.apply(this, arguments); }, /** @@ -2647,9 +2924,9 @@ var Body = Backgrid.Body = Backbone.View.extend({ @abstract @class Backgrid.Footer - @extends Backbone.View + @extends Backgrid.View */ -var Footer = Backgrid.Footer = Backbone.View.extend({ +var Footer = Backgrid.Footer = Backgrid.View.extend({ /** @property */ tagName: "tfoot", @@ -2687,7 +2964,7 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ By default, a Grid treats each model in a collection as a row, and each attribute in a model as a column. To render a grid you must provide a list of column metadata and a collection to the Grid constructor. Just like any - Backbone.View class, the grid is rendered as a DOM node fragment when you + Backgrid.View class, the grid is rendered as a DOM node fragment when you call render(). var grid = Backgrid.Grid({ @@ -2718,7 +2995,7 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ Row class. @class Backgrid.Grid - @extends Backbone.View + @extends Backgrid.View See: @@ -2728,7 +3005,7 @@ var Footer = Backgrid.Footer = Backbone.View.extend({ - Backgrid.Row - Backgrid.Footer */ -var Grid = Backgrid.Grid = Backbone.View.extend({ +var Grid = Backgrid.Grid = Backgrid.View.extend({ /** @property */ tagName: "table", @@ -2847,17 +3124,17 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ the it has successfully been rendered. */ render: function () { - this.$el.empty(); + this.empty(); if (this.header) { - this.$el.append(this.header.render().$el); + this.el.appendChild(this.header.render().el); } if (this.footer) { - this.$el.append(this.footer.render().$el); + this.el.appendChild(this.footer.render().el); } - this.$el.append(this.body.render().$el); + this.el.appendChild(this.body.render().el); this.delegateEvents(); @@ -2875,7 +3152,7 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ this.header && this.header.remove.apply(this.header, arguments); this.body.remove.apply(this.body, arguments); this.footer && this.footer.remove.apply(this.footer, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); + return Grid.__super__.remove.apply(this, arguments); } }); diff --git a/src/body.js b/src/body.js index 1b4890d6..2e1ce3b4 100644 --- a/src/body.js +++ b/src/body.js @@ -11,9 +11,9 @@ responsible for refreshing the rows after sorting, insertion and removal. @class Backgrid.Body - @extends Backbone.View + @extends Backgrid.View */ -var Body = Backgrid.Body = Backbone.View.extend({ +var Body = Backgrid.Body = Backgrid.View.extend({ /** @property */ tagName: "tbody", @@ -111,16 +111,12 @@ var Body = Backgrid.Body = Backbone.View.extend({ var index = collection.indexOf(model); this.rows.splice(index, 0, row); - var $el = this.$el; - var $children = $el.children(); - var $rowEl = row.render().$el; + var el = this.el; + var children = el.childNodes; + var rowEl = row.render().el; - if (index >= $children.length) { - $el.append($rowEl); - } - else { - $children.eq(index).before($rowEl); - } + if (index >= children.length) el.appendChild(rowEl); + else el.insertBefore(rowEl, children[index]); return this; }, @@ -174,9 +170,16 @@ var Body = Backgrid.Body = Backbone.View.extend({ instance as its sole parameter when done. */ refresh: function () { - for (var i = 0; i < this.rows.length; i++) { - this.rows[i].remove(); - } + var parent = this.el.parentNode; + if (parent) parent.removeChild(this.el); + + // GC the damn rows in the background + var oldRows = [].slice.apply(this.rows); + setTimeout(function () { + for (var i = 0; i < oldRows.length; i++) { + oldRows[i].remove(); + } + }, 0); this.rows = this.collection.map(function (model) { var row = new this.row({ @@ -186,10 +189,13 @@ var Body = Backgrid.Body = Backbone.View.extend({ return row; }, this); + this._unshiftEmptyRowMayBe(); this.render(); + if (parent) parent.appendChild(this.el); + this.collection.trigger("backgrid:refresh", this); return this; @@ -201,7 +207,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ row is rendered, otherwise no row is rendered. */ render: function () { - this.$el.empty(); + this.empty(); var fragment = document.createDocumentFragment(); for (var i = 0; i < this.rows.length; i++) { @@ -226,7 +232,7 @@ var Body = Backgrid.Body = Backbone.View.extend({ var row = this.rows[i]; row.remove.apply(row, arguments); } - return Backbone.View.prototype.remove.apply(this, arguments); + return Body.__super__.remove.apply(this, arguments); }, /** diff --git a/src/cell.js b/src/cell.js index 67bc30d1..8f69ee4e 100644 --- a/src/cell.js +++ b/src/cell.js @@ -12,9 +12,9 @@ @abstract @class Backgrid.CellEditor - @extends Backbone.View + @extends Backgrid.View */ -var CellEditor = Backgrid.CellEditor = Backbone.View.extend({ +var CellEditor = Backgrid.CellEditor = Backgrid.View.extend({ /** Initializer. @@ -44,7 +44,7 @@ var CellEditor = Backgrid.CellEditor = Backbone.View.extend({ */ postRender: function (model, column) { if (column == null || column.get("name") == this.column.get("name")) { - this.$el.focus(); + this.el.focus(); } return this; } @@ -89,7 +89,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ InputCellEditor.__super__.initialize.apply(this, arguments); if (options.placeholder) { - this.$el.attr("placeholder", options.placeholder); + this.el.setAttribute("placeholder", options.placeholder); } }, @@ -99,7 +99,7 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ */ render: function () { var model = this.model; - this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model)); + this.el.setAttribute("value", this.formatter.fromRaw(model.get(this.column.get("name")), model)); return this; }, @@ -121,7 +121,6 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ @param {Event} e */ saveOrCancel: function (e) { - var formatter = this.formatter; var model = this.model; var column = this.column; @@ -129,13 +128,10 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ var command = new Command(e); var blurred = e.type === "blur"; - if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || - command.save() || blurred) { + if (command.moveUp() || command.moveDown() || command.moveLeft() || + command.moveRight() || command.save() || blurred) { - e.preventDefault(); - e.stopPropagation(); - - var val = this.$el.val(); + var val = this.el.value; var newValue = formatter.toRaw(val, model); if (_.isUndefined(newValue)) { model.trigger("backgrid:error", model, column, val); @@ -144,23 +140,33 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ model.set(column.get("name"), newValue); model.trigger("backgrid:edited", model, column, command); } + + e.preventDefault(); + e.stopPropagation(); } // esc else if (command.cancel()) { + model.trigger("backgrid:edited", model, column, command); // undo e.stopPropagation(); - model.trigger("backgrid:edited", model, column, command); } }, postRender: function (model, column) { if (column == null || column.get("name") == this.column.get("name")) { // move the cursor to the end on firefox if text is right aligned - if (this.$el.css("text-align") === "right") { - var val = this.$el.val(); - this.$el.focus().val(null).val(val); + var el = this.el, textAlign; + if (window.getComputedStyle) { + textAlign = window.getComputedStyle(el).textAlign; } - else this.$el.focus(); + else if (el.currentStyle) textAlign = el.currentStyle.textAlign; + if (textAlign === "right") { + var value = el.value; + el.focus(); + el.value = null; + el.value = value; + } + else el.focus(); } return this; } @@ -177,9 +183,9 @@ var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ @abstract @class Backgrid.Cell - @extends Backbone.View + @extends Backgrid.View */ -var Cell = Backgrid.Cell = Backbone.View.extend({ +var Cell = Backgrid.Cell = Backgrid.View.extend({ /** @property */ tagName: "td", @@ -219,7 +225,7 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ this.column = new Column(this.column); } - var column = this.column, model = this.model, $el = this.$el; + var column = this.column, model = this.model, classes = this.el.classList; var formatter = Backgrid.resolveNameToClass(column.get("formatter") || this.formatter, "Formatter"); @@ -231,9 +237,8 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ this.formatter = formatter; this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor"); - this.listenTo(model, "change:" + column.get("name"), function () { - if (!$el.hasClass("editor")) this.render(); + if (!classes.contains("editor")) this.render(); }); this.listenTo(model, "backgrid:error", this.renderError); @@ -243,14 +248,15 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ var changed = column.changedAttributes(); for (var key in changed) { if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); + if (changed[key]) classes.add(key); + else classes.remove(key); } } }); - if (Backgrid.callByNeed(column.editable(), column, model)) $el.addClass("editable"); - if (Backgrid.callByNeed(column.sortable(), column, model)) $el.addClass("sortable"); - if (Backgrid.callByNeed(column.renderable(), column, model)) $el.addClass("renderable"); + if (Backgrid.callByNeed(column.editable(), column, model)) classes.add("editable"); + if (Backgrid.callByNeed(column.sortable(), column, model)) classes.add("sortable"); + if (Backgrid.callByNeed(column.renderable(), column, model)) classes.add("renderable"); }, /** @@ -258,9 +264,10 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ model's raw value for this cell's column. */ render: function () { - this.$el.empty(); + this.empty(); var model = this.model; - this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model)); + this.el.appendChild(document.createTextNode( + this.formatter.fromRaw(model.get(this.column.get("name")), model))); this.delegateEvents(); return this; }, @@ -301,10 +308,10 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ // Need to redundantly undelegate events for Firefox this.undelegateEvents(); - this.$el.empty(); - this.$el.append(this.currentEditor.$el); + this.empty(); + this.el.appendChild(this.currentEditor.el); this.currentEditor.render(); - this.$el.addClass("editor"); + this.el.classList.add("editor"); model.trigger("backgrid:editing", model, column, this, this.currentEditor); } @@ -315,7 +322,7 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ */ renderError: function (model, column) { if (column == null || column.get("name") == this.column.get("name")) { - this.$el.addClass("error"); + this.el.classList.add("error"); } }, @@ -323,11 +330,11 @@ var Cell = Backgrid.Cell = Backbone.View.extend({ Removes the editor and re-render in display mode. */ exitEditMode: function () { - this.$el.removeClass("error"); + this.el.classList.remove("error"); this.currentEditor.remove(); this.stopListening(this.currentEditor); delete this.currentEditor; - this.$el.removeClass("editor"); + this.el.classList.remove("editor"); this.render(); }, @@ -396,15 +403,17 @@ var UriCell = Backgrid.UriCell = Cell.extend({ }, render: function () { - this.$el.empty(); - var rawValue = this.model.get(this.column.get("name")); - var formattedValue = this.formatter.fromRaw(rawValue, this.model); - this.$el.append($("", { - tabIndex: -1, - href: rawValue, - title: this.title || formattedValue, - target: this.target - }).text(formattedValue)); + this.empty(); + var model = this.model; + var rawValue = model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, model); + var a = document.createElement("a"); + a.tabIndex = -1; + a.href = rawValue; + a.title = this.title || formattedValue; + a.target = this.target; + a.appendChild(document.createTextNode(formattedValue)); + this.el.appendChild(a); this.delegateEvents(); return this; } @@ -427,14 +436,16 @@ var EmailCell = Backgrid.EmailCell = StringCell.extend({ formatter: EmailFormatter, render: function () { - this.$el.empty(); + this.empty(); var model = this.model; - var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.append($("", { - tabIndex: -1, - href: "mailto:" + formattedValue, - title: formattedValue - }).text(formattedValue)); + var rawValue = model.get(this.column.get("name")); + var formattedValue = this.formatter.fromRaw(rawValue, model); + var a = document.createElement("a"); + a.tabIndex = -1; + a.href = "mailto:" + rawValue; + a.title = formattedValue; + a.appendChild(document.createTextNode(formattedValue)); + this.el.appendChild(a); this.delegateEvents(); return this; } @@ -674,7 +685,7 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ render: function () { var model = this.model; var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.prop("checked", val); + this.el.defaultChecked = val; return this; }, @@ -702,23 +713,23 @@ var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ // skip ahead to `change` when space is pressed if (command.passThru() && e.type != "change") return true; if (command.cancel()) { - e.stopPropagation(); model.trigger("backgrid:edited", model, column, command); + e.stopPropagation(); } - var $el = this.$el; + var el = this.el; if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - var val = formatter.toRaw($el.prop("checked"), model); + var val = formatter.toRaw(el.checked, model); model.set(column.get("name"), val); model.trigger("backgrid:edited", model, column, command); + e.preventDefault(); + e.stopPropagation(); } else if (e.type == "change") { - var val = formatter.toRaw($el.prop("checked"), model); + var val = formatter.toRaw(el.checked, model); model.set(column.get("name"), val); - $el.focus(); + el.focus(); } } @@ -749,15 +760,15 @@ var BooleanCell = Backgrid.BooleanCell = Cell.extend({ uncheck otherwise. */ render: function () { - this.$el.empty(); + this.empty(); var model = this.model, column = this.column; var editable = Backgrid.callByNeed(column.editable(), column, model); - this.$el.append($("", { - tabIndex: -1, - type: "checkbox", - checked: this.formatter.fromRaw(model.get(column.get("name")), model), - disabled: !editable - })); + var input = document.createElement("input"); + input.tabIndex = -1; + input.type = "checkbox"; + input.checked = this.formatter.fromRaw(model.get(this.column.get("name")), model); + input.disabled = !editable; + this.el.appendChild(input); this.delegateEvents(); return this; } @@ -782,9 +793,6 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ "keydown": "close" }, - /** @property {function(Object, ?Object=): string} template */ - template: _.template('', null, {variable: null}), - setOptionValues: function (optionValues) { this.optionValues = optionValues; this.optionValues = _.result(this, "optionValues"); @@ -792,17 +800,21 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ setMultiple: function (multiple) { this.multiple = multiple; - this.$el.prop("multiple", multiple); + this.el.multiple = multiple; }, _renderOptions: function (nvps, selectedValues) { - var options = ''; + var options = []; for (var i = 0; i < nvps.length; i++) { - options = options + this.template({ - text: nvps[i][0], - value: nvps[i][1], - selected: _.indexOf(selectedValues, nvps[i][1]) > -1 - }); + var nvp = nvps[i]; + var name = nvps[i][0]; + var value = nvps[i][1]; + var option = document.createElement("option"); + option.value = value; + option.defaultSelected = _.indexOf(selectedValues, value) > -1; + option.selected = option.defaultSelected; + option.appendChild(document.createTextNode(name)); + options.push(option); } return options; }, @@ -815,7 +827,7 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ parameter. */ render: function () { - this.$el.empty(); + this.empty(); var optionValues = _.result(this, "optionValues"); var model = this.model; @@ -825,34 +837,42 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ var optionValue = null; var optionText = null; - var optionValue = null; var optgroupName = null; - var optgroup = null; + var children = []; - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; + for (var i = 0; i < optionValues.length; ++i) { + optionValue = optionValues[i]; if (_.isArray(optionValue)) { optionText = optionValue[0]; optionValue = optionValue[1]; - this.$el.append(this.template({ - text: optionText, - value: optionValue, - selected: _.indexOf(selectedValues, optionValue) > -1 - })); + var option = document.createElement("option"); + option.value = optionValue; + option.defaultSelected = _.indexOf(selectedValues, optionValue) > -1; + option.selected = option.defaultSelected; + option.appendChild(document.createTextNode(optionText)); + children.push(option); } else if (_.isObject(optionValue)) { optgroupName = optionValue.name; - optgroup = $("", { label: optgroupName }); - optgroup.append(this._renderOptions.call(this, optionValue.values, selectedValues)); - this.$el.append(optgroup); + var optgroup = document.createElement("optgroup"); + optgroup.label = optgroupName; + var options = this._renderOptions(optionValue.values, selectedValues); + for (var j = 0; j < options.length; j++) { + optgroup.appendChild(options[j]); + } + children.push(optgroup); } else { throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); } } + for (var i = 0; i < children.length; ++i) { + this.el.appendChild(children[i]); + } + this.delegateEvents(); return this; @@ -864,7 +884,20 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ save: function (e) { var model = this.model; var column = this.column; - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); + + var values = null; + var options = this.el.options, option; + if (!this.multiple) values = this.el.value || null; + else if (this.multiple) { + values = []; + for (var i = 0, l = options.length; i < l; i++) { + option = options[i]; + if (option.selected) values.push(option.value); + } + if (values.length === 0) values = null; + } + + model.set(column.get("name"), this.formatter.toRaw(values, model)); }, /** @@ -876,15 +909,15 @@ var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ var column = this.column; var command = new Command(e); if (command.cancel()) { - e.stopPropagation(); model.trigger("backgrid:edited", model, column, new Command(e)); + e.stopPropagation(); } else if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || command.moveDown() || e.type == "blur") { - e.preventDefault(); - e.stopPropagation(); this.save(e); model.trigger("backgrid:edited", model, column, new Command(e)); + e.preventDefault(); + e.stopPropagation(); } } @@ -969,7 +1002,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ @throws {TypeError} If `optionValues` is malformed. */ render: function () { - this.$el.empty(); + this.empty(); var optionValues = _.result(this, "optionValues"); var model = this.model; @@ -988,7 +1021,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ if (_.isArray(optionValue)) { var optionText = optionValue[0]; - var optionValue = optionValue[1]; + optionValue = optionValue[1]; if (optionValue == rawDatum) selectedText.push(optionText); } @@ -1008,7 +1041,7 @@ var SelectCell = Backgrid.SelectCell = Cell.extend({ } } - this.$el.append(selectedText.join(this.delimiter)); + this.el.appendChild(window.document.createTextNode(selectedText.join(this.delimiter))); } catch (ex) { if (ex instanceof TypeError) { diff --git a/src/footer.js b/src/footer.js index f31f9294..46a19e0b 100644 --- a/src/footer.js +++ b/src/footer.js @@ -12,9 +12,9 @@ @abstract @class Backgrid.Footer - @extends Backbone.View + @extends Backgrid.View */ -var Footer = Backgrid.Footer = Backbone.View.extend({ +var Footer = Backgrid.Footer = Backgrid.View.extend({ /** @property */ tagName: "tfoot", diff --git a/src/grid.js b/src/grid.js index 8494edd6..e7039137 100644 --- a/src/grid.js +++ b/src/grid.js @@ -12,7 +12,7 @@ By default, a Grid treats each model in a collection as a row, and each attribute in a model as a column. To render a grid you must provide a list of column metadata and a collection to the Grid constructor. Just like any - Backbone.View class, the grid is rendered as a DOM node fragment when you + Backgrid.View class, the grid is rendered as a DOM node fragment when you call render(). var grid = Backgrid.Grid({ @@ -43,7 +43,7 @@ Row class. @class Backgrid.Grid - @extends Backbone.View + @extends Backgrid.View See: @@ -53,7 +53,7 @@ - Backgrid.Row - Backgrid.Footer */ -var Grid = Backgrid.Grid = Backbone.View.extend({ +var Grid = Backgrid.Grid = Backgrid.View.extend({ /** @property */ tagName: "table", @@ -172,17 +172,17 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ the it has successfully been rendered. */ render: function () { - this.$el.empty(); + this.empty(); if (this.header) { - this.$el.append(this.header.render().$el); + this.el.appendChild(this.header.render().el); } if (this.footer) { - this.$el.append(this.footer.render().$el); + this.el.appendChild(this.footer.render().el); } - this.$el.append(this.body.render().$el); + this.el.appendChild(this.body.render().el); this.delegateEvents(); @@ -200,7 +200,7 @@ var Grid = Backgrid.Grid = Backbone.View.extend({ this.header && this.header.remove.apply(this.header, arguments); this.body.remove.apply(this.body, arguments); this.footer && this.footer.remove.apply(this.footer, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); + return Grid.__super__.remove.apply(this, arguments); } }); diff --git a/src/header.js b/src/header.js index f327e6e0..03502e44 100644 --- a/src/header.js +++ b/src/header.js @@ -12,9 +12,9 @@ refresh after sorting. @class Backgrid.HeaderCell - @extends Backbone.View + @extends Backgrid.View */ -var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ +var HeaderCell = Backgrid.HeaderCell = Backgrid.View.extend({ /** @property */ tagName: "th", @@ -38,23 +38,27 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ this.column = new Column(this.column); } - var column = this.column, collection = this.collection, $el = this.$el; + var column = this.column + var collection = this.collection + var el = this.el; + var classes = el.classList; this.listenTo(column, "change:editable change:sortable change:renderable", function (column) { var changed = column.changedAttributes(); for (var key in changed) { if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); + if (changed[key]) classes.add(key); + else classes.remove(key); } } }); this.listenTo(column, "change:direction", this.setCellDirection); this.listenTo(column, "change:name change:label", this.render); - if (Backgrid.callByNeed(column.editable(), column, collection)) $el.addClass("editable"); - if (Backgrid.callByNeed(column.sortable(), column, collection)) $el.addClass("sortable"); - if (Backgrid.callByNeed(column.renderable(), column, collection)) $el.addClass("renderable"); + if (Backgrid.callByNeed(column.editable(), column, collection)) classes.add("editable"); + if (Backgrid.callByNeed(column.sortable(), column, collection)) classes.add("sortable"); + if (Backgrid.callByNeed(column.renderable(), column, collection)) classes.add("renderable"); this.listenTo(collection.fullCollection || collection, "sort", this.removeCellDirection); }, @@ -64,7 +68,9 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ direction classes. */ removeCellDirection: function () { - this.$el.removeClass("ascending").removeClass("descending"); + var classes = this.el.classList; + classes.remove("ascending"); + classes.remove("descending"); this.column.set("direction", null); }, @@ -75,8 +81,10 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ otherwise. */ setCellDirection: function (column, direction) { - this.$el.removeClass("ascending").removeClass("descending"); - if (column.cid == this.column.cid) this.$el.addClass(direction); + var classes = this.el.classList; + classes.remove("ascending"); + classes.remove("descending"); + if (column.cid == this.column.cid) classes.add(direction); }, /** @@ -85,8 +93,6 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ `ascending`, `descending`, and default. */ onClick: function (e) { - e.preventDefault(); - var column = this.column; var collection = this.collection; var event = "backgrid:sort"; @@ -108,6 +114,8 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ if (sortType === "toggle") toggleSort(this, column); else cycleSort(this, column); } + + e.preventDefault(); }, /** @@ -115,19 +123,29 @@ var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ column. */ render: function () { - this.$el.empty(); + this.empty(); + + var label; + var column = this.column; var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - var label; - if(sortable){ - label = $("").text(column.get("label")).append(""); - } else { - label = document.createTextNode(column.get("label")); + + if (sortable) { + label = document.createElement("a"); + label.appendChild(document.createTextNode(this.column.get("label"))); + var caret = document.createElement("b"); + caret.className = "sort-caret"; + label.appendChild(caret); } + else label = document.createTextNode(column.get("label")); + this.el.appendChild(label); + + var classes = this.el.classList; + classes.add(column.get("name")); + + var direction = column.get("direction"); + if (direction) classes.add(direction); - this.$el.append(label); - this.$el.addClass(column.get("name")); - this.$el.addClass(column.get("direction")); this.delegateEvents(); return this; } @@ -157,7 +175,7 @@ var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ @throws {TypeError} If options.columns or options.collection is undefined. */ initialize: function () { - Backgrid.Row.prototype.initialize.apply(this, arguments); + HeaderRow.__super__.initialize.apply(this, arguments); }, makeCell: function (column, options) { @@ -176,9 +194,9 @@ var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ single row of header cells. @class Backgrid.Header - @extends Backbone.View + @extends Backgrid.View */ -var Header = Backgrid.Header = Backbone.View.extend({ +var Header = Backgrid.Header = Backgrid.View.extend({ /** @property */ tagName: "thead", @@ -209,7 +227,7 @@ var Header = Backgrid.Header = Backbone.View.extend({ Renders this table head with a single row of header cells. */ render: function () { - this.$el.append(this.row.render().$el); + this.el.appendChild(this.row.render().el); this.delegateEvents(); return this; }, @@ -221,7 +239,7 @@ var Header = Backgrid.Header = Backbone.View.extend({ */ remove: function () { this.row.remove.apply(this.row, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); + return Header.__super__.remove.apply(this, arguments); } }); diff --git a/src/preamble.js b/src/preamble.js index 6cf8dbf6..a107514b 100644 --- a/src/preamble.js +++ b/src/preamble.js @@ -9,18 +9,18 @@ // Copyright 2009, 2010 Kristopher Michael Kowal // https://github.com/kriskowal/es5-shim // ES5 15.5.4.20 -// http://es5.github.com/#x15.5.4.20 +// whitespace from: http://es5.github.io/#x15.5.4.20 var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + - "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + - "\u2029\uFEFF"; + "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + + "\u2029\uFEFF"; if (!String.prototype.trim || ws.trim()) { // http://blog.stevenlevithan.com/archives/faster-trim-javascript // http://perfectionkills.com/whitespace-deviations/ ws = "[" + ws + "]"; var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), - trimEndRegexp = new RegExp(ws + ws + "*$"); + trimEndRegexp = new RegExp(ws + ws + "*$"); String.prototype.trim = function trim() { - if (this === undefined || this === null) { + if (this === void 0 || this === null) { throw new TypeError("can't convert " + this + " to object"); } return String(this) @@ -39,8 +39,6 @@ function lpad(str, length, padstr) { return padding + str; } -var $ = Backbone.$; - var Backgrid = root.Backgrid = { Extension: {}, @@ -146,3 +144,225 @@ _.extend(Command.prototype, { this.moveRight() || this.save() || this.cancel()); } }); + +// Caches a local reference to `Element.prototype` for faster access. +var ElementProto = typeof Element != 'undefined' && Element.prototype; + +// Given an object `obj` and an operation `op`, which must be either `add` or +// `remove`, this function returns either the native implementation of the +// [add|remove]EventListener method directly or a function that delegates to +// IE's [attach|detach]Event methods. +function makeEventListener(obj, op) { + if (obj[op + 'EventListener']) return obj[op + 'EventListener']; + var func = op == 'add' ? obj.attachEvent : obj.detachEvent; + return function (eventName, listener) { + func.call(this, 'on' + eventName, listener); + }; +} + +// Caches the Element prototype's event listener methods for everything else. +var elementAddEventListener = makeEventListener(ElementProto, 'add'); +var elementRemoveEventListener = makeEventListener(ElementProto, 'remove'); + +// Find the right `Element#matches` for IE>=9 and modern browsers. +var matchesSelector = ElementProto && ElementProto.matches || + ElementProto[_.find(['webkit', 'moz', 'ms', 'o'], function (prefix) { + return !!ElementProto[prefix + 'MatchesSelector']; + }) + 'MatchesSelector'] || + // Make our own `Element#matches` for IE8 + function (selector) { + // We'll use querySelectorAll to find all element matching the selector, + // then check if the given element is included in that list. + // Executing the query on the parentNode reduces the resulting nodeList, + // document doesn't have a parentNode, though. + var nodeList = (this.parentNode || document).querySelectorAll(selector) || []; + for (var i = 0, l = nodeList.length; i < l; i++) { + if (nodeList[i] == this) return true; + } + return false; + }; + +// Cached regex to split keys for `delegate`. +var delegateEventSplitter = /^(\S+)\s*(.*)$/; + +// Cached regex to match an opening '<' of an HTML tag, possibly left-padded +// with whitespace. +var paddedLt = /^\s*2' + @@ -38,7 +38,7 @@ describe("A Body", function () { body.collection.add({ id: 4 }); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(4); expect($("
").append($trs.eq(3).clone()).html().toLowerCase().replace(/\s*4'); @@ -46,7 +46,7 @@ describe("A Body", function () { body.collection.add({ id: 5 }, {at: 1}); - $trs = body.$el.children(); + $trs = $(body.el).children(); expect($trs.length).toBe(5); expect($("
").append($trs.eq(1).clone()).html().toLowerCase().replace(/\s*5'); @@ -68,7 +68,7 @@ describe("A Body", function () { id: 4 }); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(1); expect($("
").append($trs.eq(0).clone()).html().toLowerCase().replace(/\s*4'); @@ -76,7 +76,7 @@ describe("A Body", function () { body.insertRow({ id: 5 }, {at: 0}); - $trs = body.$el.children(); + $trs = $(body.el).children(); expect($trs.length).toBe(2); expect($("
").append($trs.eq(0).clone()).html().toLowerCase().replace(/\s*5'); @@ -85,7 +85,7 @@ describe("A Body", function () { it("will remove a row from the DOM if a model is removed from its collection", function () { var m1 = body.collection.at(1); body.collection.remove(m1); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(2); expect($(body.el).html().toLowerCase().replace(/\s+2' + @@ -95,7 +95,7 @@ describe("A Body", function () { it("will remove a row from the DOM is removeRow is called directly with a model", function () { var m1 = body.collection.at(1); body.removeRow(m1); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(2); expect($(body.el).html().toLowerCase().replace(/\s+2' + @@ -113,7 +113,7 @@ describe("A Body", function () { }]); body.collection.off("backgrid:refresh", handler); expect(eventFired).toBe(true); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(1); expect($(body.el).html().toLowerCase().replace(/\s+6'); @@ -168,12 +168,12 @@ describe("A Body", function () { it("when adding to a full page", function () { col.add(new Backbone.Model({id: 4})); - expect(body.$el.find("tr").length).toBe(2); + expect($(body.el).find("tr").length).toBe(2); }); it("when removing from a full page", function () { col.remove(col.get(1)); - expect(body.$el.find("tr").length).toBe(2); + expect($(body.el).find("tr").length).toBe(2); }); }); @@ -189,11 +189,11 @@ describe("A Body", function () { }); body.render(); - expect(body.$el.find("tr.empty").length).toBe(0); + expect($(body.el).find("tr.empty").length).toBe(0); }); it("will not display the empty row if `options.emptyText` is not supplied", function () { - expect(body.$el.find("tr.empty").length).toBe(0); + expect($(body.el).find("tr.empty").length).toBe(0); col.reset(); body = new Backgrid.Body({ @@ -205,7 +205,7 @@ describe("A Body", function () { }); body.render(); - expect(body.$el.find("tr.empty").length).toBe(0); + expect($(body.el).find("tr.empty").length).toBe(0); }); it("will display the empty row if the collection is empty and `options.emptyText` is supplied", function () { @@ -220,8 +220,8 @@ describe("A Body", function () { }); body.render(); - expect(body.$el.find("tr.empty").length).toBe(1); - expect(body.$el.find("tr.empty > td").attr("colspan")).toBe("1"); + expect($(body.el).find("tr.empty").length).toBe(1); + expect($(body.el).find("tr.empty > td").attr("colspan")).toBe("1"); }); it("will clear the empty row if a new model is added to an empty collection", function () { @@ -235,16 +235,16 @@ describe("A Body", function () { collection: col }); body.render(); - expect(body.$el.find("tr.empty").length).toBe(1); + expect($(body.el).find("tr.empty").length).toBe(1); col.add({id: 4}); - expect(body.$el.find("tr.empty").length).toBe(0); + expect($(body.el).find("tr.empty").length).toBe(0); col.reset(); - expect(body.$el.find("tr.empty").length).toBe(1); + expect($(body.el).find("tr.empty").length).toBe(1); body.insertRow({id: 5}); - expect(body.$el.find("tr.empty").length).toBe(0); + expect($(body.el).find("tr.empty").length).toBe(0); }); it("#sort will throw a RangeError is direction is not ascending, descending or null", function () { @@ -453,8 +453,8 @@ describe("A Body", function () { // right people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 9})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(true); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(false); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(true); expect(callOrders[0]).toBe("exit"); expect(callOrders[1]).toBe("enter"); @@ -463,29 +463,29 @@ describe("A Body", function () { // left people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 9, shiftKey: true})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(true); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(true); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(false); // down people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 40})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(true); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(false); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(true); // up people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 38})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(true); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(true); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(false); // enter people.trigger("backgrid:edited", people.at(0), columns.at(0), new Backgrid.Command({keyCode: 13})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(false); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(false); // esc body.rows[1].cells[0].enterEditMode(); people.trigger("backgrid:edited", people.at(1), columns.at(0), new Backgrid.Command({keyCode: 27})); - expect(body.rows[0].cells[0].$el.hasClass("editor")).toBe(false); - expect(body.rows[1].cells[0].$el.hasClass("editor")).toBe(false); + expect($(body.rows[0].cells[0].el).hasClass("editor")).toBe(false); + expect($(body.rows[1].cells[0].el).hasClass("editor")).toBe(false); }); }); diff --git a/test/cell.js b/test/cell.js index ac696d67..2cd1e4f1 100644 --- a/test/cell.js +++ b/test/cell.js @@ -5,6 +5,16 @@ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors Licensed under the MIT license. */ + +function emit(element, event) { + if (element.dispatchEvent) { + element.dispatchEvent(event); + } + else if (element.fireEvent) { + element.fireEvent("on" + event.type, event); + } +} + describe("A CellEditor", function () { it("calls postRender when model triggers 'backgrid:editing'", function () { @@ -89,32 +99,35 @@ describe("An InputCellEditor", function () { it("renders a text input box with a placeholder and the model value formatted for display", function () { editor.render(); expect(editor.el).toBeAnInstanceOf(HTMLInputElement); - expect(editor.$el.attr("placeholder")).toBe("put your text here"); - expect(editor.$el.val()).toBe("title"); + expect($(editor.el).attr("placeholder")).toBe("put your text here"); + expect($(editor.el).val()).toBe("title"); }); it("saves a formatted value in the input box to the model and triggers 'backgrid:edited' from the model when tab is pressed", function () { editor.render(); - editor.$el.val("another title"); - var tab = $.Event("keydown", { keyCode: 9 }); - editor.$el.trigger(tab); + $(editor.el).val("another title"); + + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("keydown", {keyCode: 9, bubbles: true, cancelable: true})); expect(editor.model.get(editor.column.get("name"))).toBe("another title"); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toEqual(editor.model); expect(backgridEditedTriggerArgs[1]).toEqual(editor.column); expect(backgridEditedTriggerArgs[2].moveRight()).toBe(true); + document.body.removeChild(editor.el); }); it("saves a formatted value in the input box to the model and triggers 'backgrid:edited' from the model when enter is pressed", function () { editor.render(); - editor.$el.val("another title"); - var enter = $.Event("keydown", { keyCode: 13 }); - editor.$el.trigger(enter); + $(editor.el).val("another title"); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("keydown", {keyCode: 13, bubbles: true, cancelable: true})); expect(editor.model.get(editor.column.get("name"))).toBe("another title"); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toEqual(editor.model); expect(backgridEditedTriggerArgs[1]).toEqual(editor.column); expect(backgridEditedTriggerArgs[2].save()).toBe(true); + document.body.removeChild(editor.el); }); it("triggers 'backgrid:error' from the model when trying to save an invalid value", function () { @@ -125,55 +138,63 @@ describe("An InputCellEditor", function () { toRaw: jasmine.createSpy("toRaw").andReturn(undefined) }; editor.render(); - editor.$el.val("invalid value"); - var enter = $.Event("keydown", { keyCode: 13 }); - editor.$el.trigger(enter); + $(editor.el).val("invalid value"); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("keydown", {keyCode: 13, bubbles: true, cancelable: true})); expect(editor.formatter.toRaw.calls.length).toBe(1); expect(editor.formatter.toRaw).toHaveBeenCalledWith("invalid value", editor.model); expect(backgridErrorTriggerCount).toBe(1); expect(backgridErrorTriggerArgs[0]).toEqual(editor.model); expect(backgridErrorTriggerArgs[1]).toEqual(editor.column); expect(backgridErrorTriggerArgs[2]).toEqual("invalid value"); + document.body.removeChild(editor.el); editor.formatter.toRaw.reset(); - editor.$el.blur(); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("blur")); expect(backgridErrorTriggerCount).toBe(2); expect(backgridErrorTriggerArgs[0]).toEqual(editor.model); expect(backgridErrorTriggerArgs[1]).toEqual(editor.column); expect(backgridErrorTriggerArgs[2]).toEqual("invalid value"); + document.body.removeChild(editor.el); }); it("discards changes and triggers 'backgrid:edited' from the model when esc is pressed'", function () { editor.render(); - editor.$el.val("new value"); - var esc = $.Event("keydown", { keyCode: 27 }); - editor.$el.trigger(esc); + $(editor.el).val("new value"); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("keydown", {keyCode: 27, bubbles: true, cancelable: true})); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toEqual(editor.model); expect(backgridEditedTriggerArgs[1]).toEqual(editor.column); expect(backgridEditedTriggerArgs[2].cancel()).toBe(true); expect(editor.model.get(editor.column.get("name"))).toBe("title"); + document.body.removeChild(editor.el); }); it("triggers 'backgrid:edited' from the model when value hasn't changed and focus is lost", function () { editor.render(); - editor.$el.blur(); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("blur")); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toEqual(editor.model); expect(backgridEditedTriggerArgs[1]).toEqual(editor.column); expect(backgridEditedTriggerArgs[2].passThru()).toBe(true); expect(editor.model.get(editor.column.get("name"))).toBe("title"); + document.body.removeChild(editor.el); }); it("saves the value if the value is valid when going out of focus", function () { editor.render(); - editor.$el.val("another title"); - editor.$el.blur(); + $(editor.el).val("another title"); + document.body.appendChild(editor.el); + emit(editor.el, SyntheticEvent("blur")); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toEqual(editor.model); expect(backgridEditedTriggerArgs[1]).toEqual(editor.column); expect(backgridEditedTriggerArgs[2].passThru()).toBe(true); expect(editor.model.get(editor.column.get("name"))).toBe("another title"); + document.body.removeChild(editor.el); }); }); @@ -236,18 +257,18 @@ describe("A Cell", function () { column: column }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); cell.column.set("editable", false); - expect(cell.$el.hasClass("editable")).toBe(false); + expect($(cell.el).hasClass("editable")).toBe(false); cell.column.set("sortable", false); - expect(cell.$el.hasClass("sortable")).toBe(false); + expect($(cell.el).hasClass("sortable")).toBe(false); cell.column.set("renderable", false); - expect(cell.$el.hasClass("renderable")).toBe(false); + expect($(cell.el).hasClass("renderable")).toBe(false); var TrueCol = Backgrid.Column.extend({ mySortable: function () { return true; }, @@ -274,9 +295,9 @@ describe("A Cell", function () { column: column }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); column = new FalseCol({ name: "title", @@ -291,9 +312,9 @@ describe("A Cell", function () { column: column }); - expect(cell.$el.hasClass("editable")).toBe(false); - expect(cell.$el.hasClass("sortable")).toBe(false); - expect(cell.$el.hasClass("renderable")).toBe(false); + expect($(cell.el).hasClass("editable")).toBe(false); + expect($(cell.el).hasClass("sortable")).toBe(false); + expect($(cell.el).hasClass("renderable")).toBe(false); column = new Backgrid.Column({ name: "title", @@ -308,35 +329,35 @@ describe("A Cell", function () { column: column }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); }); it("renders a td with the model value formatted for display", function () { cell.render(); - expect(cell.$el.text()).toBe("title"); + expect($(cell.el).text()).toBe("title"); }); it("goes into edit mode on click", function () { cell.render(); - cell.$el.click(); - expect(cell.$el.hasClass("editor")).toBe(true); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); + expect($(cell.el).hasClass("editor")).toBe(true); }); it("goes into edit mode when `enterEditMode` is called", function () { cell.render(); cell.enterEditMode(); - expect(cell.$el.hasClass("editor")).toBe(true); + expect($(cell.el).hasClass("editor")).toBe(true); }); it("goes back into display mode when `exitEditMode` is called", function () { cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); cell.exitEditMode(); - expect(cell.$el.hasClass("editor")).toBe(false); - expect(cell.$el.text()).toBe("title"); + expect($(cell.el).hasClass("editor")).toBe(false); + expect($(cell.el).text()).toBe("title"); }); it("renders error when the editor triggers 'backgrid:error'", function () { @@ -347,21 +368,20 @@ describe("A Cell", function () { }; cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); var editor = cell.currentEditor; - editor.$el.val(undefined); + $(editor.el).val(undefined); - var enter = $.Event("keydown", { keyCode: 13 }); - editor.$el.trigger(enter); + emit(editor.el, SyntheticEvent("keydown", {keyCode: 13, bubbles: true, cancelable: true})); - expect(cell.$el.hasClass("error")).toBe(true); - expect(cell.$el.hasClass("editor")).toBe(true); + expect($(cell.el).hasClass("error")).toBe(true); + expect($(cell.el).hasClass("editor")).toBe(true); }); it("removes the editor correctly when removing the cell", function() { cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); var editor = cell.currentEditor; @@ -376,20 +396,20 @@ describe("A Cell", function () { it("refreshes during display mode", function () { cell.render(); book.set("title", "another title"); - expect(cell.$el.text()).toBe("another title"); + expect($(cell.el).text()).toBe("another title"); }); it("does not refresh during display mode if the change was silenced", function () { cell.render(); book.set("title", "another title", {silent: true}); - expect(cell.$el.text()).toBe("title"); + expect($(cell.el).text()).toBe("title"); }); it("does not refresh during edit mode", function () { cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); book.set("title", "another title"); - expect(cell.$el.find("input[type=text]").val(), "title"); + expect($(cell.el).find("input[type=text]").val(), "title"); }); }); @@ -413,7 +433,7 @@ describe("A StringCell", function () { }); cell.render(); - expect(cell.$el.hasClass("string-cell")).toBe(true); + expect($(cell.el).hasClass("string-cell")).toBe(true); }); }); @@ -442,18 +462,18 @@ describe("A UriCell", function () { it("applies a uri-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("uri-cell")).toBe(true); + expect($(cell.el).hasClass("uri-cell")).toBe(true); }); it("renders the model value in an anchor", function () { cell.render(); - expect(cell.$el.find("a").attr("href")).toBe("http://www.example.com"); - expect(cell.$el.find("a").text()).toBe("http://www.example.com"); + expect($(cell.el).find("a").attr("href")).toBe("http://www.example.com"); + expect($(cell.el).find("a").text()).toBe("http://www.example.com"); }); it("uses the supplied target or _blank", function () { cell.render(); - expect(cell.$el.find("a").attr("target")).toBe("_blank"); + expect($(cell.el).find("a").attr("target")).toBe("_blank"); cell = new Backgrid.UriCell({ model: model, @@ -461,12 +481,12 @@ describe("A UriCell", function () { target: "_self" }); cell.render(); - expect(cell.$el.find("a").attr("target")).toBe("_self"); + expect($(cell.el).find("a").attr("target")).toBe("_self"); }); it("uses the supplied title or the raw value", function () { cell.render(); - expect(cell.$el.find("a").attr("title")).toBe("http://www.example.com"); + expect($(cell.el).find("a").attr("title")).toBe("http://www.example.com"); cell = new Backgrid.UriCell({ model: model, @@ -474,7 +494,7 @@ describe("A UriCell", function () { title: "http://backgridjs.com" }); cell.render(); - expect(cell.$el.find("a").attr("title")).toBe("http://backgridjs.com"); + expect($(cell.el).find("a").attr("title")).toBe("http://backgridjs.com"); }); }); @@ -502,13 +522,13 @@ describe("An EmailCell", function () { }); it("applies a email-cell class to the cell", function () { - expect(cell.render().$el.hasClass("email-cell")).toBe(true); + expect($(cell.render().el).hasClass("email-cell")).toBe(true); }); it("renders the model value in a mailto: anchor", function () { cell.render(); - expect(cell.$el.find("a").attr("href")).toBe("mailto:email@host"); - expect(cell.$el.find("a").text()).toBe("email@host"); + expect($(cell.el).find("a").attr("href")).toBe("mailto:email@host"); + expect($(cell.el).find("a").text()).toBe("email@host"); }); }); @@ -551,7 +571,7 @@ describe("A NumberCell", function () { } }); cell.render(); - expect(cell.$el.hasClass("number-cell")).toBe(true); + expect($(cell.el).hasClass("number-cell")).toBe(true); }); }); @@ -574,13 +594,13 @@ describe("An IntegerCell", function () { it("applies an integer-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("integer-cell")).toBe(true); + expect($(cell.el).hasClass("integer-cell")).toBe(true); }); it("will render a number with no trailing decimals numbers", function () { cell.model.set("age", 1.1); cell.render(); - expect(cell.$el.text()).toBe("1"); + expect($(cell.el).text()).toBe("1"); }); it("can be extended and it's defaults overidden", function () { @@ -600,7 +620,7 @@ describe("An IntegerCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("1000"); + expect($(cell.el).text()).toBe("1000"); }); }); @@ -623,12 +643,12 @@ describe("A PercentCell", function () { it("applies an percent-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("percent-cell")).toBe(true); + expect($(cell.el).hasClass("percent-cell")).toBe(true); }); it("will render a percentage string", function () { cell.render(); - expect(cell.$el.text()).toBe("99.80%"); + expect($(cell.el).text()).toBe("99.80%"); }); it("can be extended and it's defaults overidden", function () { @@ -650,7 +670,7 @@ describe("A PercentCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("1099.00pct"); + expect($(cell.el).text()).toBe("1099.00pct"); }); }); @@ -701,7 +721,7 @@ describe("A DatetimeCell", function () { it("applies a datetime-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("datetime-cell")).toBe(true); + expect($(cell.el).hasClass("datetime-cell")).toBe(true); }); it("renders a placeholder for different datetime formats for the editor according to configuration", function () { @@ -752,7 +772,7 @@ describe("A DatetimeCell", function () { model: new Backbone.Model({ datetime: null }), column: column }); - expect(cell.$el.html()).toBe(''); + expect($(cell.el).html()).toBe(''); }); }); @@ -775,13 +795,13 @@ describe("A DateCell", function () { it("applies a date-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("date-cell")).toBe(true); + expect($(cell.el).hasClass("date-cell")).toBe(true); }); it("will render a date with no time", function () { cell.model.set("date", "2000-01-01T00:00:00.000Z"); cell.render(); - expect(cell.$el.text()).toBe("2000-01-01"); + expect($(cell.el).text()).toBe("2000-01-01"); }); }); @@ -804,13 +824,13 @@ describe("A TimeCell", function () { it("applies a time-cell class to the cell", function () { cell.render(); - expect(cell.$el.hasClass("time-cell")).toBe(true); + expect($(cell.el).hasClass("time-cell")).toBe(true); }); it("will render a time with no date", function () { cell.model.set("date", "2000-01-01T00:00:00.000Z"); cell.render(); - expect(cell.$el.text()).toBe("00:00:00"); + expect($(cell.el).text()).toBe("00:00:00"); }); }); @@ -836,47 +856,47 @@ describe("A BooleanCell", function () { }); it("applies a boolean-cell class to the cell", function () { - expect(cell.render().$el.hasClass("boolean-cell")).toBe(true); + expect($(cell.render().el).hasClass("boolean-cell")).toBe(true); }); it("has a display mode that renders a checkbox with the checkbox checked if the model value is true, not checked otherwise", function () { cell.render(); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); model.set("ate", false); cell.render(); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(false); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(false); }); it("renders a disabled checkbox if not editable", function () { cell.column.set("editable", false); cell.render(); - expect(cell.$el.find(":checkbox").prop("disabled")).toBe(true); + expect($(cell.el).find(":checkbox").prop("disabled")).toBe(true); cell.column.set("editable", true); cell.render(); - expect(cell.$el.find(":checkbox").prop("disabled")).toBe(false); + expect($(cell.el).find(":checkbox").prop("disabled")).toBe(false); }); it("goes into edit mode after clicking the cell with the checkbox intact", function () { cell.render(); - cell.$el.click(); - expect(cell.$el.hasClass("editor")).toBe(true); - expect(cell.$el.find(":checkbox").length).toBe(1); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); + expect($(cell.el).hasClass("editor")).toBe(true); + expect($(cell.el).find(":checkbox").length).toBe(1); }); it("goes into edit mode after calling `enterEditMode` with the checkbox intact", function () { cell.render(); cell.enterEditMode(); - expect(cell.$el.hasClass("editor")).toBe(true); - expect(cell.$el.find(":checkbox").length).toBe(1); + expect($(cell.el).hasClass("editor")).toBe(true); + expect($(cell.el).find(":checkbox").length).toBe(1); }); it("goes back to display mode after calling `exitEditMode`", function () { cell.render(); cell.enterEditMode(); cell.exitEditMode(); - expect(cell.$el.hasClass("editor")).toBe(false); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).hasClass("editor")).toBe(false); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); }); it("triggers `backgrid:edited` when the checkbox goes out of focus", function () { @@ -888,25 +908,26 @@ describe("A BooleanCell", function () { }); cell.render(); - cell.$el.click(); - cell.$el.find(":checkbox").blur(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); + emit($(cell.el).find(":checkbox")[0], SyntheticEvent("blur")); expect(backgridEditedTriggerCount).toBe(1); expect(backgridEditedTriggerArgs[0]).toBe(cell.model); expect(backgridEditedTriggerArgs[1]).toBe(cell.column); expect(backgridEditedTriggerArgs[2].passThru()).toBe(true); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); cell.render(); - cell.$el.click(); - cell.currentEditor.$el.mousedown(); - cell.$el.find(":checkbox").blur(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); + $(cell.currentEditor.el).mousedown(); + $(cell.el).find(":checkbox").blur(); expect(backgridEditedTriggerCount).toBe(1); }); it("saves a boolean value to the model when the checkbox is toggled", function () { cell.render(); cell.enterEditMode(); - cell.$el.find(":checkbox").prop("checked", false).change(); + $(cell.el).find(":checkbox")[0].checked = false; + emit($(cell.el).find(":checkbox")[0], SyntheticEvent("change")); expect(cell.model.get(cell.column.get("name"))).toBe(false); }); @@ -914,25 +935,25 @@ describe("A BooleanCell", function () { it("refreshes during display mode", function () { cell.render(); model.set("ate", false); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(false); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(false); model.set("ate", true); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); }); it("does not refresh during display mode if the change was silenced", function () { cell.render(); model.set("ate", false, {silent: true}); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); model.set("ate", false); model.set("ate", true, {silent: true}); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); }); it("does not refresh during edit mode", function () { cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); model.set("ate", false); - expect(cell.$el.find(":checkbox").prop("checked")).toBe(true); + expect($(cell.el).find(":checkbox").prop("checked")).toBe(true); }); }); @@ -966,7 +987,7 @@ describe("A SelectCellEditor", function () { }]; }); - it("renders a select box using a list if nvps", function () { + it("renders a select box using a list of nvps", function () { // single selection var editor = new Backgrid.SelectCellEditor({ @@ -983,7 +1004,7 @@ describe("A SelectCellEditor", function () { editor.setOptionValues(optionValues); editor.render(); expect(editor.el.tagName).toBe("SELECT"); - var $options = editor.$el.children(); + var $options = $(editor.el).children(); expect($options.length).toBe(2); expect($options.eq(0).val()).toBe("1"); expect($options.eq(0).prop("selected")).toBe(false); @@ -1009,7 +1030,7 @@ describe("A SelectCellEditor", function () { editor.render(); expect(editor.el.tagName).toBe("SELECT"); expect(editor.el.multiple).toBe(true); - var $options = editor.$el.children(); + var $options = $(editor.el).children(); expect($options.length).toBe(2); expect($options.eq(0).val()).toBe("1"); expect($options.eq(0).prop("selected")).toBe(true); @@ -1038,7 +1059,7 @@ describe("A SelectCellEditor", function () { }); editor.render(); expect(editor.el.tagName).toBe("SELECT"); - var $options = editor.$el.children(); + var $options = $(editor.el).children(); expect($options.length).toBe(2); expect($options.eq(0).val()).toBe("1"); expect($options.eq(0).prop("selected")).toBe(false); @@ -1066,7 +1087,7 @@ describe("A SelectCellEditor", function () { editor.render(); expect(editor.el.tagName).toBe("SELECT"); expect(editor.el.multiple).toBe(true); - var $options = editor.$el.children(); + var $options = $(editor.el).children(); expect($options.length).toBe(2); expect($options.eq(0).val()).toBe("1"); expect($options.eq(0).prop("selected")).toBe(true); @@ -1092,7 +1113,7 @@ describe("A SelectCellEditor", function () { editor.setOptionValues(optionGroupValues); editor.render(); - var $optionGroups = editor.$el.children(); + var $optionGroups = $(editor.el).children(); expect($optionGroups.length).toBe(2); var $group1 = $optionGroups.eq(0); @@ -1138,7 +1159,7 @@ describe("A SelectCellEditor", function () { editor.setMultiple(true); editor.setOptionValues(optionGroupValues); editor.render(); - var $optionGroups = editor.$el.children(); + var $optionGroups = $(editor.el).children(); expect($optionGroups.length).toBe(2); var $group1 = $optionGroups.eq(0); @@ -1188,7 +1209,7 @@ describe("A SelectCellEditor", function () { return optionGroupValues; }); editor.render(); - var $optionGroups = editor.$el.children(); + var $optionGroups = $(editor.el).children(); expect($optionGroups.length).toBe(2); var $group1 = $optionGroups.eq(0); @@ -1236,7 +1257,7 @@ describe("A SelectCellEditor", function () { return optionGroupValues; }); editor.render(); - var $optionGroups = editor.$el.children(); + var $optionGroups = $(editor.el).children(); expect($optionGroups.length).toBe(2); var $group1 = $optionGroups.eq(0); @@ -1268,6 +1289,8 @@ describe("A SelectCellEditor", function () { expect($group2Options.eq(2).text()).toBe("Maize"); }); + // NOTE: change events are fired on most browsers on blur, the rest are when + // the value has been changed it("saves the value to the model on change", function () { // single selection @@ -1287,7 +1310,7 @@ describe("A SelectCellEditor", function () { spyOn(editor.formatter, "toRaw").andCallThrough(); - editor.$el.val(1).change(); + emit($(editor.el).val(1)[0], SyntheticEvent("blur")); expect(editor.formatter.toRaw).toHaveBeenCalledWith("1", editor.model); expect(editor.formatter.toRaw.calls.length).toBe(1); expect(editor.model.get(editor.column.get("name"))).toBe("1"); @@ -1310,12 +1333,12 @@ describe("A SelectCellEditor", function () { spyOn(editor.formatter, "toRaw").andCallThrough(); - editor.$el.val([1, 2]).change(); + emit($(editor.el).val([1, 2])[0], SyntheticEvent("blur")); expect(editor.formatter.toRaw).toHaveBeenCalledWith(["1", "2"], editor.model); expect(editor.formatter.toRaw.calls.length).toBe(1); expect(editor.model.get(editor.column.get("name"))).toEqual(["1", "2"]); - editor.$el.val(null).change(); + emit($(editor.el).val(null)[0], SyntheticEvent("blur")); expect(editor.formatter.toRaw).toHaveBeenCalledWith(null, editor.model); expect(editor.formatter.toRaw.calls.length).toBe(2); expect(editor.model.get(editor.column.get("name"))).toBe(null); @@ -1333,7 +1356,7 @@ describe("A SelectCellEditor", function () { editor.setOptionValues([["Boy", "1"]]); editor.render(); - editor.$el.blur(); + emit(editor.el, SyntheticEvent("blur")); expect(editor.model.get(editor.column.get("name"))).toBe("1"); }); @@ -1350,10 +1373,10 @@ describe("A SelectCellEditor", function () { return [["Male", "M"], ["Female", "F"]]; }); editor.render(); - expect(editor.$el.find("option").eq(0).val()).toBe("M"); - expect(editor.$el.find("option").eq(0).text()).toBe("Male"); - expect(editor.$el.find("option").eq(1).val()).toBe("F"); - expect(editor.$el.find("option").eq(1).text()).toBe("Female"); + expect($(editor.el).find("option").eq(0).val()).toBe("M"); + expect($(editor.el).find("option").eq(0).text()).toBe("Male"); + expect($(editor.el).find("option").eq(1).val()).toBe("F"); + expect($(editor.el).find("option").eq(1).text()).toBe("Female"); expect(editor.el.tagName).toBe("SELECT"); }); @@ -1401,7 +1424,7 @@ describe("A SelectCell", function () { cell.render(); - expect(cell.$el.hasClass("select-cell")).toBe(true); + expect($(cell.el).hasClass("select-cell")).toBe(true); }); it("renders the label of the selected option in display mode", function () { @@ -1420,7 +1443,7 @@ describe("A SelectCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("Girl"); + expect($(cell.el).text()).toBe("Girl"); var cell = new (Backgrid.SelectCell.extend({ optionValues: optionGroupValues @@ -1435,7 +1458,7 @@ describe("A SelectCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("Banana"); + expect($(cell.el).text()).toBe("Banana"); // multiple selection var cell = new (Backgrid.SelectCell.extend({ @@ -1451,7 +1474,7 @@ describe("A SelectCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("Boy, Girl"); + expect($(cell.el).text()).toBe("Boy, Girl"); var cell = new (Backgrid.SelectCell.extend({ optionValues: optionGroupValues @@ -1466,7 +1489,7 @@ describe("A SelectCell", function () { }); cell.render(); - expect(cell.$el.text()).toBe("Apple, Banana"); + expect($(cell.el).text()).toBe("Apple, Banana"); }); it("can accept optionValues that is a function", function () { @@ -1482,7 +1505,7 @@ describe("A SelectCell", function () { model: new Backbone.Model({"gender": "m"}) }); cell.render(); - expect(cell.$el.text()).toBe("Male"); + expect($(cell.el).text()).toBe("Male"); }); describe("when the model value has changed", function () { @@ -1514,21 +1537,21 @@ describe("A SelectCell", function () { it("refreshes during display mode", function () { cell.render(); model.set("gender", 1); - expect(cell.$el.text()).toBe("Boy"); + expect($(cell.el).text()).toBe("Boy"); }); it("does not refresh during display mode if the change was silenced", function () { cell.render(); model.set("gender", 1, {silent: true}); - expect(cell.$el.text()).toBe("Girl"); + expect($(cell.el).text()).toBe("Girl"); }); it("does not refresh during edit mode", function () { cell.render(); - cell.$el.click(); + emit(cell.el, SyntheticEvent("click", {bubbles: true})); model.set("gender", 1); - expect(cell.$el.find("option[selected]").val()).toBe("2"); - expect(cell.$el.find("option[selected]").text()).toBe("Girl"); + expect($(cell.el).find("option[selected]").val()).toBe("2"); + expect($(cell.el).find("option[selected]").text()).toBe("Girl"); }); }); diff --git a/test/header.js b/test/header.js index cf035ce7..9cc02bbe 100644 --- a/test/header.js +++ b/test/header.js @@ -5,6 +5,16 @@ Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors Licensed under the MIT license. */ + +function emit(element, event) { + if (element.dispatchEvent) { + element.dispatchEvent(event); + } + else if (element.fireEvent) { + element.fireEvent("on" + event.type, event); + } +} + describe("A HeaderCell", function () { var col; @@ -21,18 +31,23 @@ describe("A HeaderCell", function () { }); cell.render(); + document.body.appendChild(cell.el); + }); + + afterEach(function () { + document.body.removeChild(cell.el); }); it("renders a table header cell with the label text and an optional anchor with sort-caret", function () { expect(cell.el.tagName).toBe("TH"); - expect(cell.$el.find("a").text()).toBe("id"); - expect(cell.$el.find(".sort-caret").length).toBe(1); + expect($(cell.el).find("a").text()).toBe("id"); + expect($(cell.el).find(".sort-caret").length).toBe(1); cell.column.set("sortable", false); cell.render(); expect(cell.el.tagName).toBe("TH"); - expect(cell.$el.text()).toBe("id"); - expect(cell.$el.find(".sort-caret").length).toBe(0); + expect($(cell.el).text()).toBe("id"); + expect($(cell.el).find(".sort-caret").length).toBe(0); }); it("adds an editable, sortable and a renderable class to the cell if these column attributes are true", function () { @@ -41,23 +56,23 @@ describe("A HeaderCell", function () { cell: "string" }; - cell = new Backgrid.HeaderCell({ + var cell = new Backgrid.HeaderCell({ column: column, collection: col }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); cell.column.set("editable", false); - expect(cell.$el.hasClass("editable")).toBe(false); + expect($(cell.el).hasClass("editable")).toBe(false); cell.column.set("sortable", false); - expect(cell.$el.hasClass("sortable")).toBe(false); + expect($(cell.el).hasClass("sortable")).toBe(false); cell.column.set("renderable", false); - expect(cell.$el.hasClass("renderable")).toBe(false); + expect($(cell.el).hasClass("renderable")).toBe(false); var TrueCol = Backgrid.Column.extend({ mySortable: function () { return true; }, @@ -84,9 +99,9 @@ describe("A HeaderCell", function () { collection: col }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); column = new FalseCol({ name: "title", @@ -101,9 +116,9 @@ describe("A HeaderCell", function () { collection: col }); - expect(cell.$el.hasClass("editable")).toBe(false); - expect(cell.$el.hasClass("sortable")).toBe(false); - expect(cell.$el.hasClass("renderable")).toBe(false); + expect($(cell.el).hasClass("editable")).toBe(false); + expect($(cell.el).hasClass("sortable")).toBe(false); + expect($(cell.el).hasClass("renderable")).toBe(false); column = new Backgrid.Column({ name: "title", @@ -118,26 +133,26 @@ describe("A HeaderCell", function () { collection: col }); - expect(cell.$el.hasClass("editable")).toBe(true); - expect(cell.$el.hasClass("sortable")).toBe(true); - expect(cell.$el.hasClass("renderable")).toBe(true); + expect($(cell.el).hasClass("editable")).toBe(true); + expect($(cell.el).hasClass("sortable")).toBe(true); + expect($(cell.el).hasClass("renderable")).toBe(true); }); it("will rerender with the column name and/or label changes", function () { - expect(cell.$el.find("a").text(), "id"); - expect(cell.$el.hasClass("id"), true); + expect($(cell.el).find("a").text(), "id"); + expect($(cell.el).hasClass("id"), true); cell.column.set("name", "name"); - expect(cell.$el.find("name"), true); - expect(cell.$el.hasClass("name"), true); + expect($(cell.el).find("name"), true); + expect($(cell.el).hasClass("name"), true); cell.column.set("label", "Name"); - expect(cell.$el.find("a").text(), "Name"); - expect(cell.$el.hasClass("Name"), true); + expect($(cell.el).find("a").text(), "Name"); + expect($(cell.el).hasClass("Name"), true); }); it("will put a class indicating the sorting direction if `direction` is set in the column", function () { - cell = new Backgrid.HeaderCell({ + var cell = new Backgrid.HeaderCell({ column: { name: "id", cell: "integer", @@ -149,15 +164,15 @@ describe("A HeaderCell", function () { cell.render(); expect(cell.el.tagName).toBe("TH"); - expect(cell.$el.find("a").text()).toBe("id"); - expect(cell.$el.find(".sort-caret").length).toBe(1); - expect(cell.$el.hasClass("descending")).toBe(true); + expect($(cell.el).find("a").text()).toBe("id"); + expect($(cell.el).find(".sort-caret").length).toBe(1); + expect($(cell.el).hasClass("descending")).toBe(true); }); it("triggers `backgrid:sort` with the column and direction set to 'ascending' if the column's direction is not set", function () { var column, direction; cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBe("ascending"); }); @@ -166,7 +181,7 @@ describe("A HeaderCell", function () { var column, direction; cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); cell.column.set("direction", "ascending"); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBe("descending"); }); @@ -175,52 +190,54 @@ describe("A HeaderCell", function () { var column, direction; cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); cell.column.set("direction", "descending"); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBeNull(); }); it("will set the column to the correct direction when `change:direction` is triggered from the column", function () { cell.column.set("direction", "ascending"); - expect(cell.$el.hasClass("ascending")).toBe(true); + expect($(cell.el).hasClass("ascending")).toBe(true); cell.column.set("direction", "descending"); - expect(cell.$el.hasClass("descending")).toBe(true); + expect($(cell.el).hasClass("descending")).toBe(true); cell.column.set("direction", null); - expect(cell.$el.hasClass("ascending")).toBe(false); - expect(cell.$el.hasClass("descending")).toBe(false); + expect($(cell.el).hasClass("ascending")).toBe(false); + expect($(cell.el).hasClass("descending")).toBe(false); }); - it("will remove its direction CSS class if `sort` is triggered from the collection or pageableCollection#fullCollection", function () { + it("will remove its direction CSS class if `sort` is triggered from the collection", function () { cell.column.set("direction", "ascending"); cell.collection.comparator = "id"; cell.collection.sort(); - expect(cell.$el.hasClass("ascending")).toBe(false); - expect(cell.$el.hasClass("descending")).toBe(false); + expect($(cell.el).hasClass("ascending")).toBe(false); + expect($(cell.el).hasClass("descending")).toBe(false); + }); - col = new Backbone.PageableCollection(col.toJSON(), { + it("will remove its direction CSS class if `sort` is triggered from the pageableCollection#fullCollection", function () { + var pagedCol = new Backbone.PageableCollection(col.toJSON(), { mode: "client" }); - col.setSorting("id", 1); - cell = new Backgrid.HeaderCell({ + pagedCol.setSorting("id", 1); + var cell = new Backgrid.HeaderCell({ column: { name: "id", cell: "integer" }, - collection: col + collection: pagedCol }); cell.column.set("direction", "ascending"); cell.collection.fullCollection.comparator = "id"; cell.collection.fullCollection.sort(); - expect(cell.$el.hasClass("ascending")).toBe(false); - expect(cell.$el.hasClass("descending")).toBe(false); + expect($(cell.el).hasClass("ascending")).toBe(false); + expect($(cell.el).hasClass("descending")).toBe(false); }); it("with `sortType` set to `toggle`, triggers `backgrid:sort` with the column and direction set to 'ascending' if the column's direction is not set", function () { var column, direction; cell.column.set("sortType", "toggle"); cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBe("ascending"); }); @@ -230,7 +247,7 @@ describe("A HeaderCell", function () { cell.column.set("sortType", "toggle"); cell.column.set("direction", "ascending"); cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBe("descending"); }); @@ -240,7 +257,7 @@ describe("A HeaderCell", function () { cell.column.set("sortType", "toggle"); cell.column.set("direction", "descending"); cell.collection.on("backgrid:sort", function (col, dir) { column = col; direction = dir; }); - cell.$el.find("a").click(); + emit($(cell.el).find("a")[0], SyntheticEvent("click", {bubbles: true})); expect(column).toBe(cell.column); expect(direction).toBe("ascending"); }); @@ -286,7 +303,7 @@ describe("A HeaderRow", function () { }); it("renders a row of header cells", function () { - expect(row.$el[0].tagName).toBe("TR"); + expect(row.el.tagName).toBe("TR"); var th1 = $(row.el.childNodes[0]); expect(th1.hasClass("editable")).toBe(true); expect(th1.hasClass("sortable")).toBe(true); @@ -305,15 +322,15 @@ describe("A HeaderRow", function () { }); it("resets the carets of the non-sorting columns", function () { - row.$el.find("a").eq(0).click(); // ascending - row.$el.find("a").eq(1).click(); // ascending, resets the previous - expect(row.$el.find("a").eq(0).hasClass("ascending")).toBe(false); - expect(row.$el.find("a").eq(1).hasClass("ascending")).toBe(false); + $(row.el).find("a").eq(0).click(); // ascending + $(row.el).find("a").eq(1).click(); // ascending, resets the previous + expect($(row.el).find("a").eq(0).hasClass("ascending")).toBe(false); + expect($(row.el).find("a").eq(1).hasClass("ascending")).toBe(false); }); it("inserts or removes a cell if a column is added or removed", function () { row.columns.add({name: "price", cell: "number"}); - expect(row.$el.children().length).toBe(3); + expect(row.el.childNodes.length).toBe(3); var lastTh = $(row.el.lastChild); expect(lastTh.hasClass("editable")).toBe(true); expect(lastTh.hasClass("sortable")).toBe(true); @@ -323,12 +340,12 @@ describe("A HeaderRow", function () { expect(lastTh.find("a > b:last-child").eq(0).hasClass("sort-caret")).toBe(true); row.columns.add({name: "publisher", cell: "string", renderable: false}); - expect(row.$el.children().length).toBe(4); - expect(row.$el.children().last().find("a").text()).toBe("publisher"); - expect(row.$el.children().last().hasClass("renderable")).toBe(false); + expect($(row.el).children().length).toBe(4); + expect($(row.el).children().last().find("a").text()).toBe("publisher"); + expect($(row.el).children().last().hasClass("renderable")).toBe(false); row.columns.remove(row.columns.first()); - expect(row.$el.children().length).toBe(3); + expect(row.el.childNodes.length).toBe(3); var firstTh = $(row.el.firstChild); expect(firstTh.hasClass("editable")).toBe(true); expect(firstTh.hasClass("sortable")).toBe(true); @@ -379,7 +396,7 @@ describe("A Header", function () { }); it("renders a header with a row of header cells", function () { - expect(head.$el[0].tagName).toBe("THEAD"); + expect(head.el.tagName).toBe("THEAD"); var th1 = $(head.row.el.childNodes[0]); expect(th1.hasClass("editable")).toBe(true); diff --git a/test/index.html b/test/index.html index df74e9db..d1c38719 100644 --- a/test/index.html +++ b/test/index.html @@ -6,8 +6,14 @@ Backgrid.js Jasmine Spec Runner + + + + diff --git a/test/row.js b/test/row.js index 63b36364..9713bbe4 100644 --- a/test/row.js +++ b/test/row.js @@ -26,7 +26,7 @@ describe("A Row", function () { expect(row.el.tagName).toBe("TR"); - var $tds = row.$el.children(); + var $tds = $(row.el).children(); expect($tds.eq(0).text()).toBe("name"); expect($tds.eq(1).text()).toBe("18"); }); @@ -45,17 +45,17 @@ describe("A Row", function () { row.render(); - var $tds = row.$el.children(); + var $tds = $(row.el).children(); expect($tds.eq(0).text()).toBe("name"); expect($tds.eq(0).hasClass("renderable")).toBe(true); row.columns.at(0).set("renderable", false); - $tds = row.$el.children(); + $tds = $(row.el).children(); expect($tds.eq(0).text()).toBe("name"); expect($tds.eq(0).hasClass("renderable")).toBe(false); row.columns.at(0).set("renderable", true); - $tds = row.$el.children(); + $tds = $(row.el).children(); expect($tds.eq(0).text()).toBe("name"); expect($tds.eq(0).hasClass("renderable")).toBe(true); }); @@ -76,17 +76,17 @@ describe("A Row", function () { row.render(); row.columns.add({name: "age", cell: "integer"}); - var $tds = row.$el.children(); + var $tds = $(row.el).children(); expect($tds.length).toBe(2); expect($tds.eq(1).text()).toBe("18"); row.columns.add({name: "birthday", cell: "date", renderable: false}); - $tds = row.$el.children(); + $tds = $(row.el).children(); expect($tds.length).toBe(3); expect($tds.last().text()).toBe("1987-06-05"); row.columns.remove(row.columns.first()); - $tds = row.$el.children(); + $tds = $(row.el).children(); expect($tds.length).toBe(2); expect($tds.first().text()).toBe("18"); expect($tds.last().text()).toBe("1987-06-05"); diff --git a/test/vendor/js/classList.js b/test/vendor/js/classList.js new file mode 100644 index 00000000..47eddeec --- /dev/null +++ b/test/vendor/js/classList.js @@ -0,0 +1,70 @@ +(function () { + +if (typeof window.Element === "undefined" || "classList" in document.documentElement) return; + +var prototype = Array.prototype, + push = prototype.push, + splice = prototype.splice, + join = prototype.join; + +function DOMTokenList(el) { + this.el = el; + // The className needs to be trimmed and split on whitespace + // to retrieve a list of classes. + var classes = el.className.replace(/^\s+|\s+$/g,'').split(/\s+/); + for (var i = 0; i < classes.length; i++) { + push.call(this, classes[i]); + } +}; + +DOMTokenList.prototype = { + add: function(token) { + if(this.contains(token)) return; + push.call(this, token); + this.el.className = this.toString(); + }, + contains: function(token) { + return this.el.className.indexOf(token) != -1; + }, + item: function(index) { + return this[index] || null; + }, + remove: function(token) { + if (!this.contains(token)) return; + for (var i = 0; i < this.length; i++) { + if (this[i] == token) break; + } + splice.call(this, i, 1); + this.el.className = this.toString(); + }, + toString: function() { + return join.call(this, ' '); + }, + toggle: function(token) { + if (!this.contains(token)) { + this.add(token); + } else { + this.remove(token); + } + + return this.contains(token); + } +}; + +window.DOMTokenList = DOMTokenList; + +function defineElementGetter (obj, prop, getter) { + if (Object.defineProperty) { + Object.defineProperty(obj, prop,{ + get : getter + }); + } else { + obj.__defineGetter__(prop, getter); + } +} + +defineElementGetter(Element.prototype, 'classList', function () { + return new DOMTokenList(this); +}); + +})(); diff --git a/test/vendor/js/synthetic-dom-events.js b/test/vendor/js/synthetic-dom-events.js new file mode 100644 index 00000000..d48b7079 --- /dev/null +++ b/test/vendor/js/synthetic-dom-events.js @@ -0,0 +1,236 @@ +!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.SyntheticEvent=e():"undefined"!=typeof global?global.SyntheticEvent=e():"undefined"!=typeof self&&(self.SyntheticEvent=e())}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o