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*; + +var View = Backgrid.View = Backbone.View.extend({ + + constructor: function () { + this._domEvents = []; + View.__super__.constructor.apply(this, arguments); + }, + + preRender: function () { + return this; + }, + + postRender: function () { + return this; + }, + + show: function () { + this.el.style.display = this._previousDisplay; + return this; + }, + + hide: function () { + this._previousDisplay = this.el.style.display; + this.el.style.display = "none"; + return this; + }, + + empty: function () { + var el = this.el; + while (el.firstChild) el.removeChild(el.firstChild); + return this; + }, + + // Delegate to `querySelectorAll` for element lookup, scoped to DOM elements + // within the current view. This should be preferred to global lookups where + // possible. + $: function(selector) { + return this.el.querySelectorAll(selector); + }, + + // Remove this view by taking the element out of the DOM, remove all the DOM + // event listeners attached to it, and remove any applicable Backbone.Events + // listeners. + remove: function() { + this.undelegateEvents(); + var el = this.el; + var parentNode = el.parentNode; + if (parentNode) parentNode.removeChild(el); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. If element is string, create or find that element and + // change this view's element to it. Otherwise, assume it is a DOM element + // and change this view's element to it. + setElement: function(element, delegate) { + if (this.el) this.undelegateEvents(); + if (typeof element == 'string') { + if (paddedLt.test(element)) { + var el = document.createElement('div'); + el.innerHTML = element; + this.el = el.firstChild; + } + else this.el = document.querySelector(element); + } + else this.el = element; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, not + // `change`, `submit`, and `reset` in Internet Explorer, not `focusin` and + // `focusout` in Firefox, and not `mouseenter` and `mouseleave` for Chrome < + // 30 and Safari. You should use `Backbone.View` if a greater cross-browser + // compatibility is desired. + // + // Pass the event name, selector and the bound method to `_delegateEvents` + // for each mapping in `events`. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + var _delegateEvents = this._delegateEvents; + for (var key in events) { + var method = events[key]; + if (typeof method != 'function') method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = method.bind && method.bind(this) || _.bind(method, this); + _delegateEvents.call(this, eventName, selector, method); + } + + return this; + }, + + // Make a event delegation handler for the given `eventName` and `selector` + // and attach it to `this.el`. + // If selector is empty, the method will be bound to `this.el`. If not, a + // new handler that will recursively traverse up the event target's DOM + // hierarchy looking for a node that matches the selector. If one is found, + // the event's `delegateTarget` property is set to it and the return the + // result of calling bound `method` with the parameters given to the + // handler. + _delegateEvents: function(eventName, selector, method) { + var root = this.el, handler; + if (!selector) handler = method; + else handler = function (e) { + var node = e.target || e.srcElement; + for (; node && node != root; node = node.parentNode) { + if (matchesSelector.call(node, selector)) { + e.delegateTarget = node; + return method.apply(this, arguments); + } + } + }; + + elementAddEventListener.call(root, eventName, handler, false); + this._domEvents.push({eventName: eventName, handler: handler}); + }, + + // Delegates to `_undelegateEvents` so `BaseView` subclasses can override + // the default event undelegation routine. + undelegateEvents: function() { + this._undelegateEvents(); + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + _undelegateEvents: function() { + var el = this.el, domEvents = this._domEvents, i, l, item; + if (el) { + for (i = 0, l = domEvents.length; i < l; i++) { + item = domEvents[i]; + elementRemoveEventListener.call(el, item.eventName, item.handler, false); + } + this._domEvents = []; + } + }, + + // Ensure that the View has a DOM element to render into. If `this.el` + // exists, it must be a string, a function or a DOM element. If it is a + // string or a DOM element, pass it through `setElement`. If it is a + // function, pass its result to `setElement`. Otherwise, create an element + // from the `id`, `className`, `tagName` and `attributes` properties, and + // pass it to `setElement`. + _ensureElement: function() { + if (!this.el) { + var el = document.createElement(_.result(this, 'tagName')); + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + for (var k in attrs) { + el.setAttribute(k, attrs[k]); + } + this.setElement(el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + +}); + /* backgrid http://github.com/wyuenho/backgrid @@ -628,9 +848,9 @@ _.extend(SelectFormatter.prototype, { @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. @@ -660,7 +880,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; } @@ -705,7 +925,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); } }, @@ -715,7 +935,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; }, @@ -737,7 +957,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; @@ -745,13 +964,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) { - - e.preventDefault(); - e.stopPropagation(); + if (command.moveUp() || command.moveDown() || command.moveLeft() || + command.moveRight() || command.save() || blurred) { - 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); @@ -760,23 +976,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; } @@ -793,9 +1019,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", @@ -835,7 +1061,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"); @@ -847,9 +1073,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); @@ -859,14 +1084,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"); }, /** @@ -874,9 +1100,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; }, @@ -917,10 +1144,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); } @@ -931,7 +1158,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"); } }, @@ -939,11 +1166,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(); }, @@ -1012,15 +1239,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; } @@ -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*; + +var View = Backgrid.View = Backbone.View.extend({ + + constructor: function () { + this._domEvents = []; + View.__super__.constructor.apply(this, arguments); + }, + + preRender: function () { + return this; + }, + + postRender: function () { + return this; + }, + + show: function () { + this.el.style.display = this._previousDisplay; + return this; + }, + + hide: function () { + this._previousDisplay = this.el.style.display; + this.el.style.display = "none"; + return this; + }, + + empty: function () { + var el = this.el; + while (el.firstChild) el.removeChild(el.firstChild); + return this; + }, + + // Delegate to `querySelectorAll` for element lookup, scoped to DOM elements + // within the current view. This should be preferred to global lookups where + // possible. + $: function(selector) { + return this.el.querySelectorAll(selector); + }, + + // Remove this view by taking the element out of the DOM, remove all the DOM + // event listeners attached to it, and remove any applicable Backbone.Events + // listeners. + remove: function() { + this.undelegateEvents(); + var el = this.el; + var parentNode = el.parentNode; + if (parentNode) parentNode.removeChild(el); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. If element is string, create or find that element and + // change this view's element to it. Otherwise, assume it is a DOM element + // and change this view's element to it. + setElement: function(element, delegate) { + if (this.el) this.undelegateEvents(); + if (typeof element == 'string') { + if (paddedLt.test(element)) { + var el = document.createElement('div'); + el.innerHTML = element; + this.el = el.firstChild; + } + else this.el = document.querySelector(element); + } + else this.el = element; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, not + // `change`, `submit`, and `reset` in Internet Explorer, not `focusin` and + // `focusout` in Firefox, and not `mouseenter` and `mouseleave` for Chrome < + // 30 and Safari. You should use `Backbone.View` if a greater cross-browser + // compatibility is desired. + // + // Pass the event name, selector and the bound method to `_delegateEvents` + // for each mapping in `events`. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + var _delegateEvents = this._delegateEvents; + for (var key in events) { + var method = events[key]; + if (typeof method != 'function') method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = method.bind && method.bind(this) || _.bind(method, this); + _delegateEvents.call(this, eventName, selector, method); + } + + return this; + }, + + // Make a event delegation handler for the given `eventName` and `selector` + // and attach it to `this.el`. + // If selector is empty, the method will be bound to `this.el`. If not, a + // new handler that will recursively traverse up the event target's DOM + // hierarchy looking for a node that matches the selector. If one is found, + // the event's `delegateTarget` property is set to it and the return the + // result of calling bound `method` with the parameters given to the + // handler. + _delegateEvents: function(eventName, selector, method) { + var root = this.el, handler; + if (!selector) handler = method; + else handler = function (e) { + var node = e.target || e.srcElement; + for (; node && node != root; node = node.parentNode) { + if (matchesSelector.call(node, selector)) { + e.delegateTarget = node; + return method.apply(this, arguments); + } + } + }; + + elementAddEventListener.call(root, eventName, handler, false); + this._domEvents.push({eventName: eventName, handler: handler}); + }, + + // Delegates to `_undelegateEvents` so `BaseView` subclasses can override + // the default event undelegation routine. + undelegateEvents: function() { + this._undelegateEvents(); + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + _undelegateEvents: function() { + var el = this.el, domEvents = this._domEvents, i, l, item; + if (el) { + for (i = 0, l = domEvents.length; i < l; i++) { + item = domEvents[i]; + elementRemoveEventListener.call(el, item.eventName, item.handler, false); + } + this._domEvents = []; + } + }, + + // Ensure that the View has a DOM element to render into. If `this.el` + // exists, it must be a string, a function or a DOM element. If it is a + // string or a DOM element, pass it through `setElement`. If it is a + // function, pass its result to `setElement`. Otherwise, create an element + // from the `id`, `className`, `tagName` and `attributes` properties, and + // pass it to `setElement`. + _ensureElement: function() { + if (!this.el) { + var el = document.createElement(_.result(this, 'tagName')); + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + for (var k in attrs) { + el.setAttribute(k, attrs[k]); + } + this.setElement(el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + +}); diff --git a/src/row.js b/src/row.js index 2cff8354..2a17a735 100644 --- a/src/row.js +++ b/src/row.js @@ -12,9 +12,9 @@ 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", @@ -45,15 +45,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]); } }); @@ -85,7 +85,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++) { @@ -109,7 +109,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); } }); @@ -119,9 +119,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", @@ -145,7 +145,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); diff --git a/test/.jshintrc b/test/.jshintrc index c4c2db78..672a2713 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -30,6 +30,8 @@ "waitsFor": false, "Backbone": false, "Backgrid": false, + "jasmine": false, + "SyntheticEvent": false, "_": false } } diff --git a/test/body.js b/test/body.js index 2aad6264..2632b11f 100644 --- a/test/body.js +++ b/test/body.js @@ -26,7 +26,7 @@ describe("A Body", function () { it("renders table rows using the given column definitions and collection", function () { expect(body.el.tagName).toBe("TBODY"); - var $trs = body.$el.children(); + var $trs = $(body.el).children(); expect($trs.length).toBe(3); expect($(body.el).html().toLowerCase().replace(/\s*