From b72dd0fb62d14a2b73a1e29f71e99a1a1d781230 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 13 May 2013 16:53:55 +0100 Subject: [PATCH 01/74] Enabled strict mode, and fixed issues raised. --- src/backend.csv.js | 3 ++- src/backend.dataproxy.js | 3 ++- src/backend.elasticsearch.js | 7 ++++--- src/backend.gdocs.js | 3 ++- src/backend.memory.js | 3 ++- src/model.js | 5 +++-- src/view.flot.js | 2 +- src/view.grid.js | 1 + src/view.map.js | 11 +++++------ src/view.multiview.js | 3 ++- src/view.slickgrid.js | 1 + src/view.timeline.js | 1 + src/widget.facetviewer.js | 1 + src/widget.fields.js | 3 ++- src/widget.filtereditor.js | 1 + src/widget.queryeditor.js | 1 + src/widget.valuefilter.js | 1 + 17 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/backend.csv.js b/src/backend.csv.js index aacfc5e9..e00aba78 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -4,10 +4,11 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file) (function(my) { + "use strict"; my.__type__ = 'csv'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## fetch // diff --git a/src/backend.dataproxy.js b/src/backend.dataproxy.js index ffe36792..ae05bf2f 100644 --- a/src/backend.dataproxy.js +++ b/src/backend.dataproxy.js @@ -3,6 +3,7 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; (function(my) { + "use strict"; my.__type__ = 'dataproxy'; // URL for the dataproxy my.dataproxy_url = '//jsonpdataproxy.appspot.com'; @@ -12,7 +13,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## load // diff --git a/src/backend.elasticsearch.js b/src/backend.elasticsearch.js index 82ba52de..5d02965f 100644 --- a/src/backend.elasticsearch.js +++ b/src/backend.elasticsearch.js @@ -3,10 +3,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; (function($, my) { + "use strict"; my.__type__ = 'elasticsearch'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## ElasticSearch Wrapper // @@ -200,8 +201,8 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; fields: fieldData }); }) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); return dfd.promise(); }; diff --git a/src/backend.gdocs.js b/src/backend.gdocs.js index 4d18aa9b..27eaee38 100644 --- a/src/backend.gdocs.js +++ b/src/backend.gdocs.js @@ -3,10 +3,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; (function(my) { + "use strict"; my.__type__ = 'gdocs'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Google spreadsheet backend // diff --git a/src/backend.memory.js b/src/backend.memory.js index 07e14fd6..861a0636 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -3,10 +3,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.Memory = this.recline.Backend.Memory || {}; (function(my) { + "use strict"; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Data Wrapper // diff --git a/src/model.js b/src/model.js index 1e6e6441..b6a9461a 100644 --- a/src/model.js +++ b/src/model.js @@ -3,9 +3,10 @@ this.recline = this.recline || {}; this.recline.Model = this.recline.Model || {}; (function(my) { + "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ @@ -298,7 +299,7 @@ my.Record = Backbone.Model.extend({ // // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { - val = this.getFieldValueUnrendered(field); + var val = this.getFieldValueUnrendered(field); if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } diff --git a/src/view.flot.js b/src/view.flot.js index b50c6000..c1802218 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -4,7 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; // ## Graph view for a Dataset using Flot graphing library. // // Initialization arguments (in a hash in first parameter): diff --git a/src/view.grid.js b/src/view.grid.js index 2a6a2b55..f826b76a 100644 --- a/src/view.grid.js +++ b/src/view.grid.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## (Data) Grid Dataset View // // Provides a tabular view on a Dataset. diff --git a/src/view.map.js b/src/view.map.js index d6de52fc..1c463882 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -4,7 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; // ## Map view for a Dataset using Leaflet mapping library. // // This view allows to plot gereferenced records on a map. The location @@ -123,7 +123,7 @@ my.Map = Backbone.View.extend({ // } infobox: function(record) { var html = ''; - for (key in record.attributes){ + for (var key in record.attributes){ if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ html += '
' + key + ': '+ record.attributes[key] + '
'; } @@ -172,8 +172,7 @@ my.Map = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - - htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); $(this.el).html(htmls); this.$map = this.el.find('.panel.map'); this.redraw(); @@ -326,7 +325,7 @@ my.Map = Backbone.View.extend({ if (!(docs instanceof Array)) docs = [docs]; _.each(docs,function(doc){ - for (key in self.features._layers){ + for (var key in self.features._layers){ if (self.features._layers[key].feature.properties.cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); } @@ -560,7 +559,7 @@ my.MapMenu = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); $(this.el).html(htmls); if (this._geomReady() && this.model.fields.length){ diff --git a/src/view.multiview.js b/src/view.multiview.js index 8542488b..e770d386 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -5,6 +5,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## MultiView // // Manage multiple views together along with query editor etc. Usage: @@ -519,7 +520,7 @@ my.parseQueryString = function(q) { // Parse the query string out of the URL hash my.parseHashQueryString = function() { - q = my.parseHashUrl(window.location.hash).query; + var q = my.parseHashUrl(window.location.hash).query; return my.parseQueryString(q); }; diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index e4780fd8..48d0bad2 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## SlickGrid Dataset View // // Provides a tabular view on a Dataset, based on SlickGrid. diff --git a/src/view.timeline.js b/src/view.timeline.js index 87cdabe4..c4c7da69 100644 --- a/src/view.timeline.js +++ b/src/view.timeline.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // turn off unnecessary logging from VMM Timeline if (typeof VMM !== 'undefined') { VMM.debug = false; diff --git a/src/widget.facetviewer.js b/src/widget.facetviewer.js index 396bc8eb..311fa2b0 100644 --- a/src/widget.facetviewer.js +++ b/src/widget.facetviewer.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## FacetViewer // diff --git a/src/widget.fields.js b/src/widget.fields.js index 13078d2f..2d490091 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -20,7 +20,8 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; + my.Fields = Backbone.View.extend({ className: 'recline-fields-view', template: ' \ diff --git a/src/widget.filtereditor.js b/src/widget.filtereditor.js index 665c851b..ee6ea56f 100644 --- a/src/widget.filtereditor.js +++ b/src/widget.filtereditor.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.FilterEditor = Backbone.View.extend({ className: 'recline-filter-editor well', diff --git a/src/widget.queryeditor.js b/src/widget.queryeditor.js index 891983de..a3e594b1 100644 --- a/src/widget.queryeditor.js +++ b/src/widget.queryeditor.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', diff --git a/src/widget.valuefilter.js b/src/widget.valuefilter.js index 60c08e65..3b57e296 100644 --- a/src/widget.valuefilter.js +++ b/src/widget.valuefilter.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.ValueFilter = Backbone.View.extend({ className: 'recline-filter-editor well', From 38256b455b4a43bee1403de15c60a874f285ee42 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 13 May 2013 18:47:38 +0100 Subject: [PATCH 02/74] Enable strict mode on the pager widget, too. --- src/widget.pager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget.pager.js b/src/widget.pager.js index 7d2e1e47..5579e00d 100644 --- a/src/widget.pager.js +++ b/src/widget.pager.js @@ -4,6 +4,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.Pager = Backbone.View.extend({ className: 'recline-pager', From 8ea1a81c0acfe3727b7e3faad177dfd800c273ae Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 13 May 2013 17:59:03 +0100 Subject: [PATCH 03/74] Updated code and tests to stop recline clobbering view.el. Issue #350. --- src/view.flot.js | 24 +++++++++---------- src/view.grid.js | 24 +++++++++---------- src/view.map.js | 40 +++++++++++++++----------------- src/view.multiview.js | 27 +++++++++++---------- src/view.slickgrid.js | 3 +-- src/view.timeline.js | 5 ++-- src/widget.facetviewer.js | 9 ++++--- src/widget.fields.js | 5 ++-- src/widget.filtereditor.js | 5 ++-- src/widget.pager.js | 7 +++--- src/widget.queryeditor.js | 5 ++-- src/widget.valuefilter.js | 5 ++-- test/view.flot.test.js | 4 ++-- test/view.map.test.js | 8 +++---- test/view.multiview.test.js | 4 ++-- test/view.timeline.test.js | 2 +- test/widget.filtereditor.test.js | 36 ++++++++++++++-------------- test/widget.valuefilter.test.js | 23 +++++++++--------- 18 files changed, 111 insertions(+), 125 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index c1802218..0bda4d18 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -38,7 +38,6 @@ my.Flot = Backbone.View.extend({ var self = this; this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; - this.el = $(this.el); _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); this.needToRedraw = false; this.model.bind('change', this.render); @@ -64,15 +63,15 @@ my.Flot = Backbone.View.extend({ self.state.set(self.editor.state.toJSON()); self.redraw(); }); - this.elSidebar = this.editor.el; + this.elSidebar = this.editor.$el; }, render: function() { var self = this; var tmplData = this.model.toTemplateJSON(); var htmls = Mustache.render(this.template, tmplData); - $(this.el).html(htmls); - this.$graph = this.el.find('.panel.graph'); + this.$el.html(htmls); + this.$graph = this.$el.find('.panel.graph'); this.$graph.on("plothover", this._toolTip); return this; }, @@ -82,7 +81,7 @@ my.Flot = Backbone.View.extend({ // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with // Uncaught Invalid dimensions for plot, width = 0, height = 0 // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' - var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); + var areWeVisible = !jQuery.expr.filters.hidden(this.el); if ((!areWeVisible || this.model.records.length === 0)) { this.needToRedraw = true; return; @@ -403,7 +402,6 @@ my.FlotControls = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); this.model.fields.bind('reset', this.render); this.model.fields.bind('add', this.render); @@ -415,7 +413,7 @@ my.FlotControls = Backbone.View.extend({ var self = this; var tmplData = this.model.toTemplateJSON(); var htmls = Mustache.render(this.template, tmplData); - this.el.html(htmls); + this.$el.html(htmls); // set up editor from state if (this.state.get('graphType')) { @@ -439,7 +437,7 @@ my.FlotControls = Backbone.View.extend({ // Private: Helper function to select an option from a select list // _selectOption: function(id,value){ - var options = this.el.find(id + ' select > option'); + var options = this.$el.find(id + ' select > option'); if (options) { options.each(function(opt){ if (this.value == value) { @@ -451,16 +449,16 @@ my.FlotControls = Backbone.View.extend({ }, onEditorSubmit: function(e) { - var select = this.el.find('.editor-group select'); + var select = this.$el.find('.editor-group select'); var $editor = this; - var $series = this.el.find('.editor-series select'); + var $series = this.$el.find('.editor-series select'); var series = $series.map(function () { return $(this).val(); }); var updatedState = { series: $.makeArray(series), - group: this.el.find('.editor-group select').val(), - graphType: this.el.find('.editor-type select').val() + group: this.$el.find('.editor-group select').val(), + graphType: this.$el.find('.editor-type select').val() }; this.state.set(updatedState); }, @@ -477,7 +475,7 @@ my.FlotControls = Backbone.View.extend({ }, this.model.toTemplateJSON()); var htmls = Mustache.render(this.templateSeriesEditor, data); - this.el.find('.editor-series-group').append(htmls); + this.$el.find('.editor-series-group').append(htmls); return this; }, diff --git a/src/view.grid.js b/src/view.grid.js index f826b76a..394b205d 100644 --- a/src/view.grid.js +++ b/src/view.grid.js @@ -16,7 +16,6 @@ my.Grid = Backbone.View.extend({ initialize: function(modelEtc) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render', 'onHorizontalScroll'); this.model.records.bind('add', this.render); this.model.records.bind('reset', this.render); @@ -60,7 +59,7 @@ my.Grid = Backbone.View.extend({ onHorizontalScroll: function(e) { var currentScroll = $(e.target).scrollLeft(); - this.el.find('.recline-grid thead tr').scrollLeft(currentScroll); + this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); }, // ====================================================== @@ -103,7 +102,7 @@ my.Grid = Backbone.View.extend({ this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions var numFields = this.fields.length; // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) - var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; + var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; var width = parseInt(Math.max(50, fullWidth / numFields), 10); // if columns extend outside viewport then remainder is 0 var remainder = Math.max(fullWidth - numFields * width,0); @@ -116,10 +115,10 @@ my.Grid = Backbone.View.extend({ } }); var htmls = Mustache.render(this.template, this.toTemplateJSON()); - this.el.html(htmls); + this.$el.html(htmls); this.model.records.forEach(function(doc) { var tr = $(''); - self.el.find('tbody').append(tr); + self.$el.find('tbody').append(tr); var newView = new my.GridRow({ model: doc, el: tr, @@ -128,12 +127,12 @@ my.Grid = Backbone.View.extend({ newView.render(); }); // hide extra header col if no scrollbar to avoid unsightly overhang - var $tbody = this.el.find('tbody')[0]; + var $tbody = this.$el.find('tbody')[0]; if ($tbody.scrollHeight <= $tbody.offsetHeight) { - this.el.find('th.last-header').hide(); + this.$el.find('th.last-header').hide(); } - this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); - this.el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); + this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); + this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); return this; }, @@ -169,7 +168,6 @@ my.GridRow = Backbone.View.extend({ initialize: function(initData) { _.bindAll(this, 'render'); this._fields = initData.fields; - this.el = $(this.el); this.model.bind('change', this.render); }, @@ -203,9 +201,9 @@ my.GridRow = Backbone.View.extend({ }, render: function() { - this.el.attr('data-id', this.model.id); + this.$el.attr('data-id', this.model.id); var html = Mustache.render(this.template, this.toTemplateJSON()); - $(this.el).html(html); + this.$el.html(html); return this; }, @@ -225,7 +223,7 @@ my.GridRow = Backbone.View.extend({ ', onEditClick: function(e) { - var editing = this.el.find('.data-table-cell-editor-editor'); + var editing = this.$el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); } diff --git a/src/view.map.js b/src/view.map.js index 1c463882..c0979a2a 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -51,7 +51,6 @@ my.Map = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this.visible = true; this.mapReady = false; // this will be the Leaflet L.Map object (setup below) @@ -103,7 +102,7 @@ my.Map = Backbone.View.extend({ this.state.bind('change', function() { self.redraw(); }); - this.elSidebar = this.menu.el; + this.elSidebar = this.menu.$el; }, // ## Customization Functions @@ -173,8 +172,8 @@ my.Map = Backbone.View.extend({ render: function() { var self = this; var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls); - this.$map = this.el.find('.panel.map'); + this.$el.html(htmls); + this.$map = this.$el.find('.panel.map'); this.redraw(); return this; }, @@ -546,7 +545,6 @@ my.MapMenu = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); this.model.fields.bind('change', this.render); this.state = new recline.Model.ObjectState(options.state); @@ -560,27 +558,27 @@ my.MapMenu = Backbone.View.extend({ render: function() { var self = this; var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls); + this.$el.html(htmls); if (this._geomReady() && this.model.fields.length){ if (this.state.get('geomField')){ this._selectOption('editor-geom-field',this.state.get('geomField')); - this.el.find('#editor-field-type-geom').attr('checked','checked').change(); + this.$el.find('#editor-field-type-geom').attr('checked','checked').change(); } else{ this._selectOption('editor-lon-field',this.state.get('lonField')); this._selectOption('editor-lat-field',this.state.get('latField')); - this.el.find('#editor-field-type-latlon').attr('checked','checked').change(); + this.$el.find('#editor-field-type-latlon').attr('checked','checked').change(); } } if (this.state.get('autoZoom')) { - this.el.find('#editor-auto-zoom').attr('checked', 'checked'); + this.$el.find('#editor-auto-zoom').attr('checked', 'checked'); } else { - this.el.find('#editor-auto-zoom').removeAttr('checked'); + this.$el.find('#editor-auto-zoom').removeAttr('checked'); } if (this.state.get('cluster')) { - this.el.find('#editor-cluster').attr('checked', 'checked'); + this.$el.find('#editor-cluster').attr('checked', 'checked'); } else { - this.el.find('#editor-cluster').removeAttr('checked'); + this.$el.find('#editor-cluster').removeAttr('checked'); } return this; }, @@ -599,17 +597,17 @@ my.MapMenu = Backbone.View.extend({ // onEditorSubmit: function(e){ e.preventDefault(); - if (this.el.find('#editor-field-type-geom').attr('checked')){ + if (this.$el.find('#editor-field-type-geom').attr('checked')){ this.state.set({ - geomField: this.el.find('.editor-geom-field > select > option:selected').val(), + geomField: this.$el.find('.editor-geom-field > select > option:selected').val(), lonField: null, latField: null }); } else { this.state.set({ geomField: null, - lonField: this.el.find('.editor-lon-field > select > option:selected').val(), - latField: this.el.find('.editor-lat-field > select > option:selected').val() + lonField: this.$el.find('.editor-lon-field > select > option:selected').val(), + latField: this.$el.find('.editor-lat-field > select > option:selected').val() }); } return false; @@ -620,11 +618,11 @@ my.MapMenu = Backbone.View.extend({ // onFieldTypeChange: function(e){ if (e.target.value == 'geom'){ - this.el.find('.editor-field-type-geom').show(); - this.el.find('.editor-field-type-latlon').hide(); + this.$el.find('.editor-field-type-geom').show(); + this.$el.find('.editor-field-type-latlon').hide(); } else { - this.el.find('.editor-field-type-geom').hide(); - this.el.find('.editor-field-type-latlon').show(); + this.$el.find('.editor-field-type-geom').hide(); + this.$el.find('.editor-field-type-latlon').show(); } }, @@ -639,7 +637,7 @@ my.MapMenu = Backbone.View.extend({ // Private: Helper function to select an option from a select list // _selectOption: function(id,value){ - var options = this.el.find('.' + id + ' > select > option'); + var options = this.$el.find('.' + id + ' > select > option'); if (options){ options.each(function(opt){ if (this.value == value) { diff --git a/src/view.multiview.js b/src/view.multiview.js index e770d386..691634ff 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -130,7 +130,6 @@ my.MultiView = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this._setupState(options.state); // Hash of 'page' views (i.e. those for whole page) keyed by page name @@ -205,7 +204,7 @@ my.MultiView = Backbone.View.extend({ }); this.model.bind('query:done', function() { self.clearNotifications(); - self.el.find('.doc-count').text(self.model.recordCount || 'Unknown'); + self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown'); }); this.model.bind('query:fail', function(error) { self.clearNotifications(); @@ -236,7 +235,7 @@ my.MultiView = Backbone.View.extend({ }, setReadOnly: function() { - this.el.addClass('recline-read-only'); + this.$el.addClass('recline-read-only'); }, render: function() { @@ -244,11 +243,11 @@ my.MultiView = Backbone.View.extend({ tmplData.views = this.pageViews; tmplData.sidebarViews = this.sidebarViews; var template = Mustache.render(this.template, tmplData); - $(this.el).html(template); + this.$el.html(template); // now create and append other views - var $dataViewContainer = this.el.find('.data-view-container'); - var $dataSidebar = this.el.find('.data-view-sidebar'); + var $dataViewContainer = this.$el.find('.data-view-container'); + var $dataSidebar = this.$el.find('.data-view-sidebar'); // the main views _.each(this.pageViews, function(view, pageName) { @@ -260,25 +259,25 @@ my.MultiView = Backbone.View.extend({ }); _.each(this.sidebarViews, function(view) { - this['$'+view.id] = view.view.el; + this['$'+view.id] = view.view.$el; $dataSidebar.append(view.view.el); }, this); var pager = new recline.View.Pager({ model: this.model.queryState }); - this.el.find('.recline-results-info').after(pager.el); + this.$el.find('.recline-results-info').after(pager.el); var queryEditor = new recline.View.QueryEditor({ model: this.model.queryState }); - this.el.find('.query-editor-here').append(queryEditor.el); + this.$el.find('.query-editor-here').append(queryEditor.el); }, // hide the sidebar if empty _showHideSidebar: function() { - var $dataSidebar = this.el.find('.data-view-sidebar'); + var $dataSidebar = this.$el.find('.data-view-sidebar'); var visibleChildren = $dataSidebar.children().filter(function() { return $(this).css("display") != "none"; }).length; @@ -291,19 +290,19 @@ my.MultiView = Backbone.View.extend({ }, updateNav: function(pageName) { - this.el.find('.navigation a').removeClass('active'); - var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); + this.$el.find('.navigation a').removeClass('active'); + var $el = this.$el.find('.navigation a[data-view="' + pageName + '"]'); $el.addClass('active'); // add/remove sidebars and hide inactive views _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { - view.view.el.show(); + view.view.$el.show(); if (view.view.elSidebar) { view.view.elSidebar.show(); } } else { - view.view.el.hide(); + view.view.$el.hide(); if (view.view.elSidebar) { view.view.elSidebar.hide(); } diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index 48d0bad2..d1f1f206 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -34,8 +34,7 @@ this.recline.View = this.recline.View || {}; my.SlickGrid = Backbone.View.extend({ initialize: function(modelEtc) { var self = this; - this.el = $(this.el); - this.el.addClass('recline-slickgrid'); + this.$el.addClass('recline-slickgrid'); _.bindAll(this, 'render'); this.model.records.bind('add', this.render); this.model.records.bind('reset', this.render); diff --git a/src/view.timeline.js b/src/view.timeline.js index c4c7da69..40aa7563 100644 --- a/src/view.timeline.js +++ b/src/view.timeline.js @@ -28,7 +28,6 @@ my.Timeline = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this.timeline = new VMM.Timeline(); this._timelineIsInitialized = false; this.model.fields.bind('reset', function() { @@ -51,7 +50,7 @@ my.Timeline = Backbone.View.extend({ render: function() { var tmplData = {}; var htmls = Mustache.render(this.template, tmplData); - this.el.html(htmls); + this.$el.html(htmls); // can only call _initTimeline once view in DOM as Timeline uses $ // internally to look up element if ($(this.elementId).length > 0) { @@ -67,7 +66,7 @@ my.Timeline = Backbone.View.extend({ }, _initTimeline: function() { - var $timeline = this.el.find(this.elementId); + var $timeline = this.$el.find(this.elementId); var data = this._timelineJSON(); this.timeline.init(data, this.elementId, this.state.get("timelineJSOptions")); this._timelineIsInitialized = true diff --git a/src/widget.facetviewer.js b/src/widget.facetviewer.js index 311fa2b0..45f9a40e 100644 --- a/src/widget.facetviewer.js +++ b/src/widget.facetviewer.js @@ -42,7 +42,6 @@ my.FacetViewer = Backbone.View.extend({ }, initialize: function(model) { _.bindAll(this, 'render'); - this.el = $(this.el); this.model.facets.bind('all', this.render); this.model.fields.bind('all', this.render); this.render(); @@ -61,17 +60,17 @@ my.FacetViewer = Backbone.View.extend({ return facet; }); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); // are there actually any facets to show? if (this.model.facets.length > 0) { - this.el.show(); + this.$el.show(); } else { - this.el.hide(); + this.$el.hide(); } }, onHide: function(e) { e.preventDefault(); - this.el.hide(); + this.$el.hide(); }, onFacetFilter: function(e) { e.preventDefault(); diff --git a/src/widget.fields.js b/src/widget.fields.js index 2d490091..bb950286 100644 --- a/src/widget.fields.js +++ b/src/widget.fields.js @@ -60,7 +60,6 @@ my.Fields = Backbone.View.extend({ initialize: function(model) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); // TODO: this is quite restrictive in terms of when it is re-run @@ -75,7 +74,7 @@ my.Fields = Backbone.View.extend({ self.model.getFieldsSummary(); self.render(); }); - this.el.find('.collapse').collapse(); + this.$el.find('.collapse').collapse(); this.render(); }, render: function() { @@ -89,7 +88,7 @@ my.Fields = Backbone.View.extend({ tmplData.fields.push(out); }); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); diff --git a/src/widget.filtereditor.js b/src/widget.filtereditor.js index ee6ea56f..d7ceefbf 100644 --- a/src/widget.filtereditor.js +++ b/src/widget.filtereditor.js @@ -89,7 +89,6 @@ my.FilterEditor = Backbone.View.extend({ 'submit form.js-add': 'onAddFilter' }, initialize: function() { - this.el = $(this.el); _.bindAll(this, 'render'); this.model.fields.bind('all', this.render); this.model.queryState.bind('change', this.render); @@ -109,13 +108,13 @@ my.FilterEditor = Backbone.View.extend({ return Mustache.render(self.filterTemplates[this.type], this); }; var out = Mustache.render(this.template, tmplData); - this.el.html(out); + this.$el.html(out); }, onAddFilterShow: function(e) { e.preventDefault(); var $target = $(e.target); $target.hide(); - this.el.find('form.js-add').show(); + this.$el.find('form.js-add').show(); }, onAddFilter: function(e) { e.preventDefault(); diff --git a/src/widget.pager.js b/src/widget.pager.js index 5579e00d..59b308db 100644 --- a/src/widget.pager.js +++ b/src/widget.pager.js @@ -25,14 +25,13 @@ my.Pager = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'render'); - this.el = $(this.el); this.model.bind('change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); - var newFrom = parseInt(this.el.find('input[name="from"]').val()); - var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; + var newFrom = parseInt(this.$el.find('input[name="from"]').val()); + var newSize = parseInt(this.$el.find('input[name="to"]').val()) - newFrom; newFrom = Math.max(newFrom, 0); newSize = Math.max(newSize, 1); this.model.set({size: newSize, from: newFrom}); @@ -53,7 +52,7 @@ my.Pager = Backbone.View.extend({ var tmplData = this.model.toJSON(); tmplData.to = this.model.get('from') + this.model.get('size'); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); diff --git a/src/widget.queryeditor.js b/src/widget.queryeditor.js index a3e594b1..1229053b 100644 --- a/src/widget.queryeditor.js +++ b/src/widget.queryeditor.js @@ -24,19 +24,18 @@ my.QueryEditor = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'render'); - this.el = $(this.el); this.model.bind('change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); - var query = this.el.find('.text-query input').val(); + var query = this.$el.find('.text-query input').val(); this.model.set({q: query}); }, render: function() { var tmplData = this.model.toJSON(); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); diff --git a/src/widget.valuefilter.js b/src/widget.valuefilter.js index 3b57e296..b176ff40 100644 --- a/src/widget.valuefilter.js +++ b/src/widget.valuefilter.js @@ -51,7 +51,6 @@ my.ValueFilter = Backbone.View.extend({ 'submit form.js-add': 'onAddFilter' }, initialize: function() { - this.el = $(this.el); _.bindAll(this, 'render'); this.model.fields.bind('all', this.render); this.model.queryState.bind('change', this.render); @@ -71,7 +70,7 @@ my.ValueFilter = Backbone.View.extend({ return Mustache.render(self.filterTemplates.term, this); }; var out = Mustache.render(this.template, tmplData); - this.el.html(out); + this.$el.html(out); }, updateFilter: function(input) { var self = this; @@ -85,7 +84,7 @@ my.ValueFilter = Backbone.View.extend({ e.preventDefault(); var $target = $(e.target); $target.hide(); - this.el.find('form.js-add').show(); + this.$el.find('form.js-add').show(); }, onAddFilter: function(e) { e.preventDefault(); diff --git a/test/view.flot.test.js b/test/view.flot.test.js index 4bf761f6..005250cd 100644 --- a/test/view.flot.test.js +++ b/test/view.flot.test.js @@ -29,8 +29,8 @@ test('initialize', function () { // check we have updated editor with state info equal(view.elSidebar.find('.editor-type select').val(), 'lines'); equal(view.elSidebar.find('.editor-group select').val(), 'x'); - var out = _.map(view.elSidebar.find('.editor-series select'), function($el) { - return $($el).val(); + var out = _.map(view.elSidebar.find('.editor-series select'), function(el) { + return $(el).val(); }); deepEqual(out, ['y', 'z']); diff --git a/test/view.map.test.js b/test/view.map.test.js index e429f646..8102ec87 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -162,13 +162,13 @@ test('Popup', function () { $('.fixtures').append(view.el); view.render(); - var marker = view.el.find('.leaflet-marker-icon').first(); + var marker = view.$el.find('.leaflet-marker-icon').first(); assertPresent(marker); _.values(view.features._layers)[0].fire('click'); - var popup = view.el.find('.leaflet-popup-content'); + var popup = view.$el.find('.leaflet-popup-content'); assertPresent(popup); @@ -195,9 +195,9 @@ test('Popup - Custom', function () { }; view.render(); - var marker = view.el.find('.leaflet-marker-icon').first(); + var marker = view.$el.find('.leaflet-marker-icon').first(); _.values(view.features._layers)[0].fire('click'); - var popup = view.el.find('.leaflet-popup-content'); + var popup = view.$el.find('.leaflet-popup-content'); assertPresent(popup); diff --git a/test/view.multiview.test.js b/test/view.multiview.test.js index db8cbb53..34cbf338 100644 --- a/test/view.multiview.test.js +++ b/test/view.multiview.test.js @@ -60,9 +60,9 @@ test('initialize state', function () { ok(explorer.state.get('currentView'), 'graph'); // check the correct view is visible - var css = explorer.el.find('.navigation a[data-view="graph"]').attr('class').split(' '); + var css = explorer.$el.find('.navigation a[data-view="graph"]').attr('class').split(' '); ok(_.contains(css, 'active'), css); - var css = explorer.el.find('.navigation a[data-view="grid"]').attr('class').split(' '); + var css = explorer.$el.find('.navigation a[data-view="grid"]').attr('class').split(' '); ok(!(_.contains(css, 'active')), css); // check pass through of view config diff --git a/test/view.timeline.test.js b/test/view.timeline.test.js index 4f6faf48..83b4a60f 100644 --- a/test/view.timeline.test.js +++ b/test/view.timeline.test.js @@ -47,7 +47,7 @@ test('render etc', function () { assertPresent('.vmm-timeline', view.el); assertPresent('.timenav', view.el); assertPresent('.timenav', view.el); - equal(view.el.find('.marker.active h4').text(), '2011'); + equal(view.$el.find('.marker.active h4').text(), '2011'); view.remove(); }); diff --git a/test/widget.filtereditor.test.js b/test/widget.filtereditor.test.js index c1acbbff..656185c2 100644 --- a/test/widget.filtereditor.test.js +++ b/test/widget.filtereditor.test.js @@ -7,10 +7,10 @@ test('basics', function () { }); $('.fixtures').append(view.el); assertPresent('.js-add-filter', view.elSidebar); - var $addForm = view.el.find('form.js-add'); + var $addForm = view.$el.find('form.js-add'); ok(!$addForm.is(":visible")); - view.el.find('.js-add-filter').click(); - ok(!view.el.find('.js-add-filter').is(":visible")); + view.$el.find('.js-add-filter').click(); + ok(!view.$el.find('.js-add-filter').is(":visible")); ok($addForm.is(":visible")); // submit the form @@ -19,7 +19,7 @@ test('basics', function () { // now check we have new filter ok(!$addForm.is(":visible")); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter-term').length, 1) equal(dataset.queryState.attributes.filters[0].field, 'country'); @@ -30,13 +30,13 @@ test('basics', function () { equal(dataset.records.length, 3); // now set a second range filter ... - view.el.find('.js-add-filter').click(); - var $addForm = view.el.find('form.js-add'); + view.$el.find('.js-add-filter').click(); + var $addForm = view.$el.find('form.js-add'); $addForm.find('select.fields').val('x'); $addForm.find('select.filterType').val('range'); $addForm.submit(); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); $editForm.find('.filter-range input').first().val('2'); $editForm.find('.filter-range input').last().val('4'); $editForm.submit(); @@ -45,15 +45,15 @@ test('basics', function () { equal(dataset.records.length, 2); // now remove filter - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); $editForm.find('.js-remove-filter').last().click(); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter').length, 1) equal(dataset.records.length, 3); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); $editForm.find('.js-remove-filter').last().click(); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter').length, 0) equal(dataset.records.length, 6); @@ -68,18 +68,18 @@ test('add 2 filters of same type', function () { $('.fixtures').append(view.el); // add 2 term filters - var $addForm = view.el.find('form.js-add'); - view.el.find('.js-add-filter').click(); + var $addForm = view.$el.find('form.js-add'); + view.$el.find('.js-add-filter').click(); $addForm.find('select.fields').val('country'); $addForm.submit(); - var $addForm = view.el.find('form.js-add'); - view.el.find('.js-add-filter').click(); + var $addForm = view.$el.find('form.js-add'); + view.$el.find('.js-add-filter').click(); $addForm.find('select.fields').val('id'); $addForm.submit(); var fields = []; - view.el.find('form.js-edit .filter-term input').each(function(idx, item) { + view.$el.find('form.js-edit .filter-term input').each(function(idx, item) { fields.push($(item).attr('data-filter-field')); }); deepEqual(fields, ['country', 'id']); @@ -94,14 +94,14 @@ test('geo_distance', function () { }); $('.fixtures').append(view.el); - var $addForm = view.el.find('form.js-add'); + var $addForm = view.$el.find('form.js-add'); // submit the form $addForm.find('select.filterType').val('geo_distance'); $addForm.find('select.fields').val('lon'); $addForm.submit(); // now check we have new filter - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter-geo_distance').length, 1) deepEqual(_.sortBy(_.keys(dataset.queryState.attributes.filters[0]),_.identity), ["distance", "field", "point", "type", "unit"]); diff --git a/test/widget.valuefilter.test.js b/test/widget.valuefilter.test.js index 6b8a86a1..09fea2ed 100644 --- a/test/widget.valuefilter.test.js +++ b/test/widget.valuefilter.test.js @@ -7,10 +7,10 @@ test('basics', function () { }); $('.fixtures').append(view.el); assertPresent('.js-add-filter', view.elSidebar); - var $addForm = view.el.find('form.js-add'); + var $addForm = view.$el.find('form.js-add'); ok(!$addForm.is(":visible")); - view.el.find('.js-add-filter').click(); - ok(!view.el.find('.js-add-filter').is(":visible")); + view.$el.find('.js-add-filter').click(); + ok(!view.$el.find('.js-add-filter').is(":visible")); ok($addForm.is(":visible")); // submit the form @@ -19,7 +19,7 @@ test('basics', function () { // now check we have new filter ok(!$addForm.is(":visible")); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter-term').length, 1); equal(dataset.queryState.attributes.filters[0].field, 'country'); @@ -30,9 +30,9 @@ test('basics', function () { equal(dataset.records.length, 3); // now remove filter - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); $editForm.find('.js-remove-filter').last().click(); - $editForm = view.el.find('form.js-edit'); + $editForm = view.$el.find('form.js-edit'); equal($editForm.find('.filter').length, 0); equal(dataset.records.length, 6); @@ -47,18 +47,19 @@ test('add 2 filters', function () { $('.fixtures').append(view.el); // add 2 term filters - var $addForm = view.el.find('form.js-add'); - view.el.find('.js-add-filter').click(); + var $addForm = view.$el.find('form.js-add'); + view.$el.find('.js-add-filter').click(); + $addForm.find('select.fields').val('country'); $addForm.submit(); - $addForm = view.el.find('form.js-add'); - view.el.find('.js-add-filter').click(); + $addForm = view.$el.find('form.js-add'); + view.$el.find('.js-add-filter').click(); $addForm.find('select.fields').val('id'); $addForm.submit(); var fields = []; - view.el.find('form.js-edit .filter-term input').each(function(idx, item) { + view.$el.find('form.js-edit .filter-term input').each(function(idx, item) { fields.push($(item).attr('data-filter-field')); }); deepEqual(fields, ['country', 'id']); From 55f808b8731c38955494a3baf62e379a58d813cf Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 13 May 2013 18:48:32 +0100 Subject: [PATCH 04/74] Updated the README to mention this breaking change. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3757ee81..177f6b1d 100755 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Possible breaking changes * Views no longer call render in initialize but must be called client code * Backend.Memory.Store attribute for holding 'records' renamed to `records` from `data` * Require new underscore.deferred vendor library for all use (jQuery no longer required if just using recline.dataset.js) +* View.el is now the raw DOM element. If you want a jQuery-wrapped version, use view.$el. #350 ### v0.5 - July 5th 2012 (first public release) From 6c8d9ec50ac328350beea1fe3f6dce463d802500 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Mon, 13 May 2013 21:48:22 +0100 Subject: [PATCH 05/74] Bumped underscore to 1.4.4 --- _includes/recline-deps.html | 2 +- test/built.html | 2 +- test/index.html | 2 +- .../underscore/{1.4.2 => 1.4.4}/underscore.js | 187 ++++++++++-------- 4 files changed, 110 insertions(+), 83 deletions(-) rename vendor/underscore/{1.4.2 => 1.4.4}/underscore.js (90%) diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index f4d09e5f..1f55dadf 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -21,7 +21,7 @@ - + diff --git a/test/built.html b/test/built.html index 9caf8822..59324fe8 100644 --- a/test/built.html +++ b/test/built.html @@ -9,7 +9,7 @@ - + diff --git a/test/index.html b/test/index.html index 5027e979..f774b07e 100644 --- a/test/index.html +++ b/test/index.html @@ -9,7 +9,7 @@ - + diff --git a/vendor/underscore/1.4.2/underscore.js b/vendor/underscore/1.4.4/underscore.js similarity index 90% rename from vendor/underscore/1.4.2/underscore.js rename to vendor/underscore/1.4.4/underscore.js index 1ebe2671..32ca0c1b 100644 --- a/vendor/underscore/1.4.2/underscore.js +++ b/vendor/underscore/1.4.4/underscore.js @@ -1,13 +1,14 @@ -// Underscore.js 1.4.2 -// http://underscorejs.org -// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. -// Underscore may be freely distributed under the MIT license. +// Underscore.js 1.4.4 +// =================== +// > http://underscorejs.org +// > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// > Underscore may be freely distributed under the MIT license. + +// Baseline setup +// -------------- (function() { - // Baseline setup - // -------------- - // Establish the root object, `window` in the browser, or `global` on the server. var root = this; @@ -24,7 +25,6 @@ var push = ArrayProto.push, slice = ArrayProto.slice, concat = ArrayProto.concat, - unshift = ArrayProto.unshift, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; @@ -61,11 +61,11 @@ } exports._ = _; } else { - root['_'] = _; + root._ = _; } // Current version. - _.VERSION = '1.4.2'; + _.VERSION = '1.4.4'; // Collection Functions // -------------------- @@ -102,6 +102,8 @@ return results; }; + var reduceError = 'Reduce of empty array with no initial value'; + // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { @@ -119,7 +121,7 @@ memo = iterator.call(context, memo, value, index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -130,7 +132,7 @@ if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); - return arguments.length > 2 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var length = obj.length; if (length !== +length) { @@ -146,7 +148,7 @@ memo = iterator.call(context, memo, obj[index], index, list); } }); - if (!initial) throw new TypeError('Reduce of empty array with no initial value'); + if (!initial) throw new TypeError(reduceError); return memo; }; @@ -177,12 +179,9 @@ // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - each(obj, function(value, index, list) { - if (!iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); }; // Determine whether all of the elements match a truth test. @@ -216,20 +215,19 @@ // Determine if the array or object contains a given value (using `===`). // Aliased as `include`. _.contains = _.include = function(obj, target) { - var found = false; - if (obj == null) return found; + if (obj == null) return false; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; - found = any(obj, function(value) { + return any(obj, function(value) { return value === target; }); - return found; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); return _.map(obj, function(value) { - return (_.isFunction(method) ? method : value[method]).apply(value, args); + return (isFunc ? method : value[method]).apply(value, args); }); }; @@ -239,10 +237,10 @@ }; // Convenience version of a common use case of `filter`: selecting only objects - // with specific `key:value` pairs. - _.where = function(obj, attrs) { - if (_.isEmpty(attrs)) return []; - return _.filter(obj, function(value) { + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { for (var key in attrs) { if (attrs[key] !== value[key]) return false; } @@ -250,6 +248,12 @@ }); }; + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + // Return the maximum element or (element-based computation). // Can't optimize arrays of integers longer than 65,535 elements. // See: https://bugs.webkit.org/show_bug.cgi?id=80797 @@ -258,7 +262,7 @@ return Math.max.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return -Infinity; - var result = {computed : -Infinity}; + var result = {computed : -Infinity, value: -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed >= result.computed && (result = {value : value, computed : computed}); @@ -272,7 +276,7 @@ return Math.min.apply(Math, obj); } if (!iterator && _.isEmpty(obj)) return Infinity; - var result = {computed : Infinity}; + var result = {computed : Infinity, value: Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); @@ -321,7 +325,7 @@ // An internal function used for aggregate "group by" operations. var group = function(obj, value, context, behavior) { var result = {}; - var iterator = lookupIterator(value); + var iterator = lookupIterator(value || _.identity); each(obj, function(value, index) { var key = iterator.call(context, value, index, obj); behavior(result, key, value); @@ -341,7 +345,7 @@ // either a string attribute to count by, or a function that returns the // criterion. _.countBy = function(obj, value, context) { - return group(obj, value, context, function(result, key, value) { + return group(obj, value, context, function(result, key) { if (!_.has(result, key)) result[key] = 0; result[key]++; }); @@ -363,12 +367,14 @@ // Safely convert anything iterable into a real, live array. _.toArray = function(obj) { if (!obj) return []; - if (obj.length === +obj.length) return slice.call(obj); + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { + if (obj == null) return 0; return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; }; @@ -379,6 +385,7 @@ // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; }; @@ -393,6 +400,7 @@ // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { + if (array == null) return void 0; if ((n != null) && !guard) { return slice.call(array, Math.max(array.length - n, 0)); } else { @@ -410,7 +418,7 @@ // Trim out all falsy values from an array. _.compact = function(array) { - return _.filter(array, function(value){ return !!value; }); + return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. @@ -439,6 +447,11 @@ // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } var initial = iterator ? _.map(array, iterator, context) : array; var results = []; var seen = []; @@ -491,6 +504,7 @@ // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { + if (list == null) return {}; var result = {}; for (var i = 0, l = list.length; i < l; i++) { if (values) { @@ -561,25 +575,23 @@ // Function (ahem) Functions // ------------------ - // Reusable constructor function for prototype setting. - var ctor = function(){}; - // Create a function bound to a given object (assigning `this`, and arguments, - // optionally). Binding with arguments is also known as `curry`. - // Delegates to **ECMAScript 5**'s native `Function.bind` if available. - // We check for `func.bind` first, to fail fast when `func` is undefined. - _.bind = function bind(func, context) { - var bound, args; + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - if (!_.isFunction(func)) throw new TypeError; - args = slice.call(arguments, 2); - return bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - ctor.prototype = func.prototype; - var self = new ctor; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (Object(result) === result) return result; - return self; + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); }; }; @@ -587,7 +599,7 @@ // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); - if (funcs.length == 0) funcs = _.functions(obj); + if (funcs.length === 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; @@ -618,25 +630,26 @@ // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. _.throttle = function(func, wait) { - var context, args, timeout, throttling, more, result; - var whenDone = _.debounce(function(){ more = throttling = false; }, wait); + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; return function() { - context = this; args = arguments; - var later = function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); timeout = null; - if (more) { - result = func.apply(context, args); - } - whenDone(); - }; - if (!timeout) timeout = setTimeout(later, wait); - if (throttling) { - more = true; - } else { - throttling = true; + previous = now; result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); } - whenDone(); return result; }; }; @@ -754,8 +767,10 @@ // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } } }); return obj; @@ -784,8 +799,10 @@ // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } } }); return obj; @@ -950,7 +967,7 @@ // Is a given object a finite number? _.isFinite = function(obj) { - return _.isNumber(obj) && isFinite(obj); + return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). @@ -996,7 +1013,9 @@ // Run a function **n** times. _.times = function(n, iterator, context) { - for (var i = 0; i < n; i++) iterator.call(context, i); + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; }; // Return a random integer between min and max (inclusive). @@ -1005,7 +1024,7 @@ max = min; min = 0; } - return min + (0 | Math.random() * (max - min + 1)); + return min + Math.floor(Math.random() * (max - min + 1)); }; // List of HTML entities for escaping. @@ -1061,7 +1080,7 @@ // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { - var id = idCounter++; + var id = ++idCounter + ''; return prefix ? prefix + id : id; }; @@ -1096,6 +1115,7 @@ // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { + var render; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. @@ -1111,11 +1131,18 @@ text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset) .replace(escaper, function(match) { return '\\' + escapes[match]; }); - source += - escape ? "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'" : - interpolate ? "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" : - evaluate ? "';\n" + evaluate + "\n__p+='" : ''; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } index = offset + match.length; + return match; }); source += "';\n"; @@ -1127,7 +1154,7 @@ source + "return __p;\n"; try { - var render = new Function(settings.variable || 'obj', '_', source); + render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; From 080188286a11879b48bea214e0dd86f04e40ea5b Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 10:12:24 +0100 Subject: [PATCH 06/74] Bumped backbone to 1.0.0. Issue #351. --- _includes/recline-deps.html | 2 +- test/built.html | 2 +- test/index.html | 2 +- vendor/backbone/0.9.2/backbone-min.js | 38 - vendor/backbone/0.9.2/backbone.js | 1431 ---------------------- vendor/backbone/1.0.0/backbone.js | 1571 +++++++++++++++++++++++++ 6 files changed, 1574 insertions(+), 1472 deletions(-) delete mode 100644 vendor/backbone/0.9.2/backbone-min.js delete mode 100644 vendor/backbone/0.9.2/backbone.js create mode 100644 vendor/backbone/1.0.0/backbone.js diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index 1f55dadf..ab67cf19 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -22,7 +22,7 @@ - + - + {% endhighlight %} diff --git a/test/index.html b/test/index.html index e15ce778..5d4f44ea 100644 --- a/test/index.html +++ b/test/index.html @@ -11,7 +11,7 @@ - + diff --git a/vendor/moment/1.6.2/moment.js b/vendor/moment/1.6.2/moment.js deleted file mode 100644 index c7901d27..00000000 --- a/vendor/moment/1.6.2/moment.js +++ /dev/null @@ -1,918 +0,0 @@ -// moment.js -// version : 1.6.2 -// author : Tim Wood -// license : MIT -// momentjs.com - -(function (Date, undefined) { - - var moment, - VERSION = "1.6.2", - round = Math.round, i, - // internal storage for language config files - languages = {}, - currentLanguage = 'en', - - // check for nodeJS - hasModule = (typeof module !== 'undefined'), - - // parameters to check for on the lang config - langConfigProperties = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'), - - // ASP.NET json date format regex - aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - - // format tokens - formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|zz?|ZZ?|LT|LL?L?L?)/g, - - // parsing tokens - parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, - - // parsing token regexes - parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 - parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenWord = /[0-9a-z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+/i, // any word characters or numbers - parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z - parseTokenT = /T/i, // T (ISO seperator) - - // preliminary iso regex - // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 - isoRegex = /^\s*\d{4}-\d\d-\d\d(T(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', - - // iso time formats and regexes - isoTimes = [ - ['HH:mm:ss.S', /T\d\d:\d\d:\d\d\.\d{1,3}/], - ['HH:mm:ss', /T\d\d:\d\d:\d\d/], - ['HH:mm', /T\d\d:\d\d/], - ['HH', /T\d\d/] - ], - - // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] - parseTimezoneChunker = /([\+\-]|\d\d)/gi, - - // getter and setter names - proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), - unitMillisecondFactors = { - 'Milliseconds' : 1, - 'Seconds' : 1e3, - 'Minutes' : 6e4, - 'Hours' : 36e5, - 'Days' : 864e5, - 'Months' : 2592e6, - 'Years' : 31536e6 - }; - - // Moment prototype object - function Moment(date, isUTC) { - this._d = date; - this._isUTC = !!isUTC; - } - - function absRound(number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); - } - } - - // Duration Constructor - function Duration(duration) { - var data = this._data = {}, - years = duration.years || duration.y || 0, - months = duration.months || duration.M || 0, - weeks = duration.weeks || duration.w || 0, - days = duration.days || duration.d || 0, - hours = duration.hours || duration.h || 0, - minutes = duration.minutes || duration.m || 0, - seconds = duration.seconds || duration.s || 0, - milliseconds = duration.milliseconds || duration.ms || 0; - - // representation for dateAddRemove - this._milliseconds = milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = months + - years * 12; - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - seconds += absRound(milliseconds / 1000); - - data.seconds = seconds % 60; - minutes += absRound(seconds / 60); - - data.minutes = minutes % 60; - hours += absRound(minutes / 60); - - data.hours = hours % 24; - days += absRound(hours / 24); - - days += weeks * 7; - data.days = days % 30; - - months += absRound(days / 30); - - data.months = months % 12; - years += absRound(months / 12); - - data.years = years; - } - - // left zero fill a number - // see http://jsperf.com/left-zero-filling for performance comparison - function leftZeroFill(number, targetLength) { - var output = number + ''; - while (output.length < targetLength) { - output = '0' + output; - } - return output; - } - - // helper function for _.addTime and _.subtractTime - function addOrSubtractDurationFromMoment(mom, duration, isAdding) { - var ms = duration._milliseconds, - d = duration._days, - M = duration._months, - currentDate; - - if (ms) { - mom._d.setTime(+mom + ms * isAdding); - } - if (d) { - mom.date(mom.date() + d * isAdding); - } - if (M) { - currentDate = mom.date(); - mom.date(1) - .month(mom.month() + M * isAdding) - .date(Math.min(currentDate, mom.daysInMonth())); - } - } - - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function dateFromArray(input) { - return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0); - } - - // format date using native date object - function formatMoment(m, inputString) { - var currentMonth = m.month(), - currentDate = m.date(), - currentYear = m.year(), - currentDay = m.day(), - currentHours = m.hours(), - currentMinutes = m.minutes(), - currentSeconds = m.seconds(), - currentMilliseconds = m.milliseconds(), - currentZone = -m.zone(), - ordinal = moment.ordinal, - meridiem = moment.meridiem; - // check if the character is a format - // return formatted string or non string. - // - // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380) - // for minification and performance - // see http://jsperf.com/object-of-functions-vs-switch for performance comparison - function replaceFunction(input) { - // create a couple variables to be used later inside one of the cases. - var a, b; - switch (input) { - // MONTH - case 'M' : - return currentMonth + 1; - case 'Mo' : - return (currentMonth + 1) + ordinal(currentMonth + 1); - case 'MM' : - return leftZeroFill(currentMonth + 1, 2); - case 'MMM' : - return moment.monthsShort[currentMonth]; - case 'MMMM' : - return moment.months[currentMonth]; - // DAY OF MONTH - case 'D' : - return currentDate; - case 'Do' : - return currentDate + ordinal(currentDate); - case 'DD' : - return leftZeroFill(currentDate, 2); - // DAY OF YEAR - case 'DDD' : - a = new Date(currentYear, currentMonth, currentDate); - b = new Date(currentYear, 0, 1); - return ~~ (((a - b) / 864e5) + 1.5); - case 'DDDo' : - a = replaceFunction('DDD'); - return a + ordinal(a); - case 'DDDD' : - return leftZeroFill(replaceFunction('DDD'), 3); - // WEEKDAY - case 'd' : - return currentDay; - case 'do' : - return currentDay + ordinal(currentDay); - case 'ddd' : - return moment.weekdaysShort[currentDay]; - case 'dddd' : - return moment.weekdays[currentDay]; - // WEEK OF YEAR - case 'w' : - a = new Date(currentYear, currentMonth, currentDate - currentDay + 5); - b = new Date(a.getFullYear(), 0, 4); - return ~~ ((a - b) / 864e5 / 7 + 1.5); - case 'wo' : - a = replaceFunction('w'); - return a + ordinal(a); - case 'ww' : - return leftZeroFill(replaceFunction('w'), 2); - // YEAR - case 'YY' : - return leftZeroFill(currentYear % 100, 2); - case 'YYYY' : - return currentYear; - // AM / PM - case 'a' : - return meridiem ? meridiem(currentHours, currentMinutes, false) : (currentHours > 11 ? 'pm' : 'am'); - case 'A' : - return meridiem ? meridiem(currentHours, currentMinutes, true) : (currentHours > 11 ? 'PM' : 'AM'); - // 24 HOUR - case 'H' : - return currentHours; - case 'HH' : - return leftZeroFill(currentHours, 2); - // 12 HOUR - case 'h' : - return currentHours % 12 || 12; - case 'hh' : - return leftZeroFill(currentHours % 12 || 12, 2); - // MINUTE - case 'm' : - return currentMinutes; - case 'mm' : - return leftZeroFill(currentMinutes, 2); - // SECOND - case 's' : - return currentSeconds; - case 'ss' : - return leftZeroFill(currentSeconds, 2); - // MILLISECONDS - case 'S' : - return ~~ (currentMilliseconds / 100); - case 'SS' : - return leftZeroFill(~~(currentMilliseconds / 10), 2); - case 'SSS' : - return leftZeroFill(currentMilliseconds, 3); - // TIMEZONE - case 'Z' : - return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2); - case 'ZZ' : - return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4); - // LONG DATES - case 'L' : - case 'LL' : - case 'LLL' : - case 'LLLL' : - case 'LT' : - return formatMoment(m, moment.longDateFormat[input]); - // DEFAULT - default : - return input.replace(/(^\[)|(\\)|\]$/g, ""); - } - } - return inputString.replace(formattingTokens, replaceFunction); - } - - // get the regex to find the next token - function getParseRegexForToken(token) { - switch (token) { - case 'DDDD': - return parseTokenThreeDigits; - case 'YYYY': - return parseTokenFourDigits; - case 'S': - case 'SS': - case 'SSS': - case 'DDD': - return parseTokenOneToThreeDigits; - case 'MMM': - case 'MMMM': - case 'ddd': - case 'dddd': - case 'a': - case 'A': - return parseTokenWord; - case 'Z': - case 'ZZ': - return parseTokenTimezone; - case 'T': - return parseTokenT; - case 'MM': - case 'DD': - case 'dd': - case 'YY': - case 'HH': - case 'hh': - case 'mm': - case 'ss': - case 'M': - case 'D': - case 'd': - case 'H': - case 'h': - case 'm': - case 's': - return parseTokenOneOrTwoDigits; - default : - return new RegExp(token.replace('\\', '')); - } - } - - // function to convert string input to date - function addTimeToArrayFromToken(token, input, datePartArray, config) { - var a; - //console.log('addTime', format, input); - switch (token) { - // MONTH - case 'M' : // fall through to MM - case 'MM' : - datePartArray[1] = (input == null) ? 0 : ~~input - 1; - break; - case 'MMM' : // fall through to MMMM - case 'MMMM' : - for (a = 0; a < 12; a++) { - if (moment.monthsParse[a].test(input)) { - datePartArray[1] = a; - break; - } - } - break; - // DAY OF MONTH - case 'D' : // fall through to DDDD - case 'DD' : // fall through to DDDD - case 'DDD' : // fall through to DDDD - case 'DDDD' : - datePartArray[2] = ~~input; - break; - // YEAR - case 'YY' : - input = ~~input; - datePartArray[0] = input + (input > 70 ? 1900 : 2000); - break; - case 'YYYY' : - datePartArray[0] = ~~Math.abs(input); - break; - // AM / PM - case 'a' : // fall through to A - case 'A' : - config.isPm = ((input + '').toLowerCase() === 'pm'); - break; - // 24 HOUR - case 'H' : // fall through to hh - case 'HH' : // fall through to hh - case 'h' : // fall through to hh - case 'hh' : - datePartArray[3] = ~~input; - break; - // MINUTE - case 'm' : // fall through to mm - case 'mm' : - datePartArray[4] = ~~input; - break; - // SECOND - case 's' : // fall through to ss - case 'ss' : - datePartArray[5] = ~~input; - break; - // MILLISECOND - case 'S' : - case 'SS' : - case 'SSS' : - datePartArray[6] = ~~ (('0.' + input) * 1000); - break; - // TIMEZONE - case 'Z' : // fall through to ZZ - case 'ZZ' : - config.isUTC = true; - a = (input + '').match(parseTimezoneChunker); - if (a && a[1]) { - config.tzh = ~~a[1]; - } - if (a && a[2]) { - config.tzm = ~~a[2]; - } - // reverse offsets - if (a && a[0] === '+') { - config.tzh = -config.tzh; - config.tzm = -config.tzm; - } - break; - } - } - - // date from string and format string - function makeDateFromStringAndFormat(string, format) { - var datePartArray = [0, 0, 1, 0, 0, 0, 0], - config = { - tzh : 0, // timezone hour offset - tzm : 0 // timezone minute offset - }, - tokens = format.match(formattingTokens), - i, parsedInput; - - for (i = 0; i < tokens.length; i++) { - parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; - string = string.replace(getParseRegexForToken(tokens[i]), ''); - addTimeToArrayFromToken(tokens[i], parsedInput, datePartArray, config); - } - // handle am pm - if (config.isPm && datePartArray[3] < 12) { - datePartArray[3] += 12; - } - // if is 12 am, change hours to 0 - if (config.isPm === false && datePartArray[3] === 12) { - datePartArray[3] = 0; - } - // handle timezone - datePartArray[3] += config.tzh; - datePartArray[4] += config.tzm; - // return - return config.isUTC ? new Date(Date.UTC.apply({}, datePartArray)) : dateFromArray(datePartArray); - } - - // compare two arrays, return the number of differences - function compareArrays(array1, array2) { - var len = Math.min(array1.length, array2.length), - lengthDiff = Math.abs(array1.length - array2.length), - diffs = 0, - i; - for (i = 0; i < len; i++) { - if (~~array1[i] !== ~~array2[i]) { - diffs++; - } - } - return diffs + lengthDiff; - } - - // date from string and array of format strings - function makeDateFromStringAndArray(string, formats) { - var output, - inputParts = string.match(parseMultipleFormatChunker) || [], - formattedInputParts, - scoreToBeat = 99, - i, - currentDate, - currentScore; - for (i = 0; i < formats.length; i++) { - currentDate = makeDateFromStringAndFormat(string, formats[i]); - formattedInputParts = formatMoment(new Moment(currentDate), formats[i]).match(parseMultipleFormatChunker) || []; - currentScore = compareArrays(inputParts, formattedInputParts); - if (currentScore < scoreToBeat) { - scoreToBeat = currentScore; - output = currentDate; - } - } - return output; - } - - // date from iso format - function makeDateFromString(string) { - var format = 'YYYY-MM-DDT', - i; - if (isoRegex.exec(string)) { - for (i = 0; i < 4; i++) { - if (isoTimes[i][1].exec(string)) { - format += isoTimes[i][0]; - break; - } - } - return parseTokenTimezone.exec(string) ? - makeDateFromStringAndFormat(string, format + ' Z') : - makeDateFromStringAndFormat(string, format); - } - return new Date(string); - } - - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture) { - var rt = moment.relativeTime[string]; - return (typeof rt === 'function') ? - rt(number || 1, !!withoutSuffix, string, isFuture) : - rt.replace(/%d/i, number || 1); - } - - function relativeTime(milliseconds, withoutSuffix) { - var seconds = round(Math.abs(milliseconds) / 1000), - minutes = round(seconds / 60), - hours = round(minutes / 60), - days = round(hours / 24), - years = round(days / 365), - args = seconds < 45 && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < 45 && ['mm', minutes] || - hours === 1 && ['h'] || - hours < 22 && ['hh', hours] || - days === 1 && ['d'] || - days <= 25 && ['dd', days] || - days <= 45 && ['M'] || - days < 345 && ['MM', round(days / 30)] || - years === 1 && ['y'] || ['yy', years]; - args[2] = withoutSuffix; - args[3] = milliseconds > 0; - return substituteTimeAgo.apply({}, args); - } - - moment = function (input, format) { - if (input === null || input === '') { - return null; - } - var date, - matched, - isUTC; - // parse Moment object - if (moment.isMoment(input)) { - date = new Date(+input._d); - isUTC = input._isUTC; - // parse string and format - } else if (format) { - if (isArray(format)) { - date = makeDateFromStringAndArray(input, format); - } else { - date = makeDateFromStringAndFormat(input, format); - } - // evaluate it as a JSON-encoded date - } else { - matched = aspNetJsonRegex.exec(input); - date = input === undefined ? new Date() : - matched ? new Date(+matched[1]) : - input instanceof Date ? input : - isArray(input) ? dateFromArray(input) : - typeof input === 'string' ? makeDateFromString(input) : - new Date(input); - } - return new Moment(date, isUTC); - }; - - // creating with utc - moment.utc = function (input, format) { - if (isArray(input)) { - return new Moment(new Date(Date.UTC.apply({}, input)), true); - } - return (format && input) ? - moment(input + ' +0000', format + ' Z').utc() : - moment(input && !parseTokenTimezone.exec(input) ? input + '+0000' : input).utc(); - }; - - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; - - // duration - moment.duration = function (input, key) { - var isDuration = moment.isDuration(input), - isNumber = (typeof input === 'number'), - duration = (isDuration ? input._data : (isNumber ? {} : input)); - - if (isNumber) { - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; - } - } - - return new Duration(duration); - }; - - // humanizeDuration - // This method is deprecated in favor of the new Duration object. Please - // see the moment.duration method. - moment.humanizeDuration = function (num, type, withSuffix) { - return moment.duration(num, type === true ? null : type).humanize(type === true ? true : withSuffix); - }; - - // version number - moment.version = VERSION; - - // default format - moment.defaultFormat = isoFormat; - - // language switching and caching - moment.lang = function (key, values) { - var i, req, - parse = []; - if (!key) { - return currentLanguage; - } - if (values) { - for (i = 0; i < 12; i++) { - parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i'); - } - values.monthsParse = values.monthsParse || parse; - languages[key] = values; - } - if (languages[key]) { - for (i = 0; i < langConfigProperties.length; i++) { - moment[langConfigProperties[i]] = languages[key][langConfigProperties[i]] || - languages.en[langConfigProperties[i]]; - } - currentLanguage = key; - } else { - if (hasModule) { - req = require('./lang/' + key); - moment.lang(key, req); - } - } - }; - - // set default language - moment.lang('en', { - months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), - monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), - weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), - weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), - longDateFormat : { - LT : "h:mm A", - L : "MM/DD/YYYY", - LL : "MMMM D YYYY", - LLL : "MMMM D YYYY LT", - LLLL : "dddd, MMMM D YYYY LT" - }, - meridiem : false, - calendar : { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[last] dddd [at] LT', - sameElse : 'L' - }, - relativeTime : { - future : "in %s", - past : "%s ago", - s : "a few seconds", - m : "a minute", - mm : "%d minutes", - h : "an hour", - hh : "%d hours", - d : "a day", - dd : "%d days", - M : "a month", - MM : "%d months", - y : "a year", - yy : "%d years" - }, - ordinal : function (number) { - var b = number % 10; - return (~~ (number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - } - }); - - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment; - }; - - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; - - // shortcut for prototype - moment.fn = Moment.prototype = { - - clone : function () { - return moment(this); - }, - - valueOf : function () { - return +this._d; - }, - - unix : function () { - return Math.floor(+this._d / 1000); - }, - - toString : function () { - return this._d.toString(); - }, - - toDate : function () { - return this._d; - }, - - utc : function () { - this._isUTC = true; - return this; - }, - - local : function () { - this._isUTC = false; - return this; - }, - - format : function (inputString) { - return formatMoment(this, inputString ? inputString : moment.defaultFormat); - }, - - add : function (input, val) { - var dur = val ? moment.duration(+val, input) : moment.duration(input); - addOrSubtractDurationFromMoment(this, dur, 1); - return this; - }, - - subtract : function (input, val) { - var dur = val ? moment.duration(+val, input) : moment.duration(input); - addOrSubtractDurationFromMoment(this, dur, -1); - return this; - }, - - diff : function (input, val, asFloat) { - var inputMoment = this._isUTC ? moment(input).utc() : moment(input).local(), - zoneDiff = (this.zone() - inputMoment.zone()) * 6e4, - diff = this._d - inputMoment._d - zoneDiff, - year = this.year() - inputMoment.year(), - month = this.month() - inputMoment.month(), - date = this.date() - inputMoment.date(), - output; - if (val === 'months') { - output = year * 12 + month + date / 30; - } else if (val === 'years') { - output = year + (month + date / 30) / 12; - } else { - output = val === 'seconds' ? diff / 1e3 : // 1000 - val === 'minutes' ? diff / 6e4 : // 1000 * 60 - val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60 - val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24 - val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 - diff; - } - return asFloat ? output : round(output); - }, - - from : function (time, withoutSuffix) { - return moment.duration(this.diff(time)).humanize(!withoutSuffix); - }, - - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, - - calendar : function () { - var diff = this.diff(moment().sod(), 'days', true), - calendar = moment.calendar, - allElse = calendar.sameElse, - format = diff < -6 ? allElse : - diff < -1 ? calendar.lastWeek : - diff < 0 ? calendar.lastDay : - diff < 1 ? calendar.sameDay : - diff < 2 ? calendar.nextDay : - diff < 7 ? calendar.nextWeek : allElse; - return this.format(typeof format === 'function' ? format.apply(this) : format); - }, - - isLeapYear : function () { - var year = this.year(); - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - }, - - isDST : function () { - return (this.zone() < moment([this.year()]).zone() || - this.zone() < moment([this.year(), 5]).zone()); - }, - - day : function (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - return input == null ? day : - this.add({ d : input - day }); - }, - - sod: function () { - return moment(this) - .hours(0) - .minutes(0) - .seconds(0) - .milliseconds(0); - }, - - eod: function () { - // end of day = start of day plus 1 day, minus 1 millisecond - return this.sod().add({ - d : 1, - ms : -1 - }); - }, - - zone : function () { - return this._isUTC ? 0 : this._d.getTimezoneOffset(); - }, - - daysInMonth : function () { - return moment(this).month(this.month() + 1).date(0).date(); - } - }; - - // helper for adding shortcuts - function makeGetterAndSetter(name, key) { - moment.fn[name] = function (input) { - var utc = this._isUTC ? 'UTC' : ''; - if (input != null) { - this._d['set' + utc + key](input); - return this; - } else { - return this._d['get' + utc + key](); - } - }; - } - - // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) - for (i = 0; i < proxyGettersAndSetters.length; i ++) { - makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase(), proxyGettersAndSetters[i]); - } - - // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') - makeGetterAndSetter('year', 'FullYear'); - - moment.duration.fn = Duration.prototype = { - weeks : function () { - return absRound(this.days() / 7); - }, - - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - this._months * 2592e6; - }, - - humanize : function (withSuffix) { - var difference = +this, - rel = moment.relativeTime, - output = relativeTime(difference, !withSuffix); - - if (withSuffix) { - output = (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output); - } - - return output; - } - }; - - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; - } - - function makeDurationAsGetter(name, factor) { - moment.duration.fn['as' + name] = function () { - return +this / factor; - }; - } - - for (i in unitMillisecondFactors) { - if (unitMillisecondFactors.hasOwnProperty(i)) { - makeDurationAsGetter(i, unitMillisecondFactors[i]); - makeDurationGetter(i.toLowerCase()); - } - } - - makeDurationAsGetter('Weeks', 6048e5); - - // CommonJS module is defined - if (hasModule) { - module.exports = moment; - } - /*global ender:false */ - if (typeof window !== 'undefined' && typeof ender === 'undefined') { - window.moment = moment; - } - /*global define:false */ - if (typeof define === "function" && define.amd) { - define("moment", [], function () { - return moment; - }); - } -})(Date); diff --git a/vendor/moment/2.0.0/moment.js b/vendor/moment/2.0.0/moment.js new file mode 100644 index 00000000..9ff57aac --- /dev/null +++ b/vendor/moment/2.0.0/moment.js @@ -0,0 +1,1400 @@ +// moment.js +// version : 2.0.0 +// author : Tim Wood +// license : MIT +// momentjs.com + +(function (undefined) { + + /************************************ + Constants + ************************************/ + + var moment, + VERSION = "2.0.0", + round = Math.round, i, + // internal storage for language config files + languages = {}, + + // check for nodeJS + hasModule = (typeof module !== 'undefined' && module.exports), + + // ASP.NET json date format regex + aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, + + // format tokens + formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, + + // parsing tokens + parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, + + // parsing token regexes + parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 + parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 + parseTokenThreeDigits = /\d{3}/, // 000 - 999 + parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 + parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 + parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic. + parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z + parseTokenT = /T/i, // T (ISO seperator) + parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + + // preliminary iso regex + // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 + isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, + isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', + + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], + ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], + ['HH:mm', /(T| )\d\d:\d\d/], + ['HH', /(T| )\d\d/] + ], + + // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] + parseTimezoneChunker = /([\+\-]|\d\d)/gi, + + // getter and setter names + proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), + unitMillisecondFactors = { + 'Milliseconds' : 1, + 'Seconds' : 1e3, + 'Minutes' : 6e4, + 'Hours' : 36e5, + 'Days' : 864e5, + 'Months' : 2592e6, + 'Years' : 31536e6 + }, + + // format function strings + formatFunctions = {}, + + // tokens to ordinalize and pad + ordinalizeTokens = 'DDD w W M D d'.split(' '), + paddedTokens = 'M D H h m s w W'.split(' '), + + formatTokenFunctions = { + M : function () { + return this.month() + 1; + }, + MMM : function (format) { + return this.lang().monthsShort(this, format); + }, + MMMM : function (format) { + return this.lang().months(this, format); + }, + D : function () { + return this.date(); + }, + DDD : function () { + return this.dayOfYear(); + }, + d : function () { + return this.day(); + }, + dd : function (format) { + return this.lang().weekdaysMin(this, format); + }, + ddd : function (format) { + return this.lang().weekdaysShort(this, format); + }, + dddd : function (format) { + return this.lang().weekdays(this, format); + }, + w : function () { + return this.week(); + }, + W : function () { + return this.isoWeek(); + }, + YY : function () { + return leftZeroFill(this.year() % 100, 2); + }, + YYYY : function () { + return leftZeroFill(this.year(), 4); + }, + YYYYY : function () { + return leftZeroFill(this.year(), 5); + }, + a : function () { + return this.lang().meridiem(this.hours(), this.minutes(), true); + }, + A : function () { + return this.lang().meridiem(this.hours(), this.minutes(), false); + }, + H : function () { + return this.hours(); + }, + h : function () { + return this.hours() % 12 || 12; + }, + m : function () { + return this.minutes(); + }, + s : function () { + return this.seconds(); + }, + S : function () { + return ~~(this.milliseconds() / 100); + }, + SS : function () { + return leftZeroFill(~~(this.milliseconds() / 10), 2); + }, + SSS : function () { + return leftZeroFill(this.milliseconds(), 3); + }, + Z : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2); + }, + ZZ : function () { + var a = -this.zone(), + b = "+"; + if (a < 0) { + a = -a; + b = "-"; + } + return b + leftZeroFill(~~(10 * a / 6), 4); + }, + X : function () { + return this.unix(); + } + }; + + function padToken(func, count) { + return function (a) { + return leftZeroFill(func.call(this, a), count); + }; + } + function ordinalizeToken(func) { + return function (a) { + return this.lang().ordinal(func.call(this, a)); + }; + } + + while (ordinalizeTokens.length) { + i = ordinalizeTokens.pop(); + formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]); + } + while (paddedTokens.length) { + i = paddedTokens.pop(); + formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); + } + formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); + + + /************************************ + Constructors + ************************************/ + + function Language() { + + } + + // Moment prototype object + function Moment(config) { + extend(this, config); + } + + // Duration Constructor + function Duration(duration) { + var data = this._data = {}, + years = duration.years || duration.year || duration.y || 0, + months = duration.months || duration.month || duration.M || 0, + weeks = duration.weeks || duration.week || duration.w || 0, + days = duration.days || duration.day || duration.d || 0, + hours = duration.hours || duration.hour || duration.h || 0, + minutes = duration.minutes || duration.minute || duration.m || 0, + seconds = duration.seconds || duration.second || duration.s || 0, + milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0; + + // representation for dateAddRemove + this._milliseconds = milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = months + + years * 12; + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + seconds += absRound(milliseconds / 1000); + + data.seconds = seconds % 60; + minutes += absRound(seconds / 60); + + data.minutes = minutes % 60; + hours += absRound(minutes / 60); + + data.hours = hours % 24; + days += absRound(hours / 24); + + days += weeks * 7; + data.days = days % 30; + + months += absRound(days / 30); + + data.months = months % 12; + years += absRound(months / 12); + + data.years = years; + } + + + /************************************ + Helpers + ************************************/ + + + function extend(a, b) { + for (var i in b) { + if (b.hasOwnProperty(i)) { + a[i] = b[i]; + } + } + return a; + } + + function absRound(number) { + if (number < 0) { + return Math.ceil(number); + } else { + return Math.floor(number); + } + } + + // left zero fill a number + // see http://jsperf.com/left-zero-filling for performance comparison + function leftZeroFill(number, targetLength) { + var output = number + ''; + while (output.length < targetLength) { + output = '0' + output; + } + return output; + } + + // helper function for _.addTime and _.subtractTime + function addOrSubtractDurationFromMoment(mom, duration, isAdding) { + var ms = duration._milliseconds, + d = duration._days, + M = duration._months, + currentDate; + + if (ms) { + mom._d.setTime(+mom + ms * isAdding); + } + if (d) { + mom.date(mom.date() + d * isAdding); + } + if (M) { + currentDate = mom.date(); + mom.date(1) + .month(mom.month() + M * isAdding) + .date(Math.min(currentDate, mom.daysInMonth())); + } + } + + // check if is an array + function isArray(input) { + return Object.prototype.toString.call(input) === '[object Array]'; + } + + // compare two arrays, return the number of differences + function compareArrays(array1, array2) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if (~~array1[i] !== ~~array2[i]) { + diffs++; + } + } + return diffs + lengthDiff; + } + + + /************************************ + Languages + ************************************/ + + + Language.prototype = { + set : function (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (typeof prop === 'function') { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + }, + + _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), + months : function (m) { + return this._months[m.month()]; + }, + + _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), + monthsShort : function (m) { + return this._monthsShort[m.month()]; + }, + + monthsParse : function (monthName) { + var i, mom, regex, output; + + if (!this._monthsParse) { + this._monthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + if (!this._monthsParse[i]) { + mom = moment([2000, i]); + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (this._monthsParse[i].test(monthName)) { + return i; + } + } + }, + + _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), + weekdays : function (m) { + return this._weekdays[m.day()]; + }, + + _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), + weekdaysShort : function (m) { + return this._weekdaysShort[m.day()]; + }, + + _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), + weekdaysMin : function (m) { + return this._weekdaysMin[m.day()]; + }, + + _longDateFormat : { + LT : "h:mm A", + L : "MM/DD/YYYY", + LL : "MMMM D YYYY", + LLL : "MMMM D YYYY LT", + LLLL : "dddd, MMMM D YYYY LT" + }, + longDateFormat : function (key) { + var output = this._longDateFormat[key]; + if (!output && this._longDateFormat[key.toUpperCase()]) { + output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + this._longDateFormat[key] = output; + } + return output; + }, + + meridiem : function (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + }, + + _calendar : { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[last] dddd [at] LT', + sameElse : 'L' + }, + calendar : function (key, mom) { + var output = this._calendar[key]; + return typeof output === 'function' ? output.apply(mom) : output; + }, + + _relativeTime : { + future : "in %s", + past : "%s ago", + s : "a few seconds", + m : "a minute", + mm : "%d minutes", + h : "an hour", + hh : "%d hours", + d : "a day", + dd : "%d days", + M : "a month", + MM : "%d months", + y : "a year", + yy : "%d years" + }, + relativeTime : function (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (typeof output === 'function') ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + }, + pastFuture : function (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); + }, + + ordinal : function (number) { + return this._ordinal.replace("%d", number); + }, + _ordinal : "%d", + + preparse : function (string) { + return string; + }, + + postformat : function (string) { + return string; + }, + + week : function (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy); + }, + _week : { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + } + }; + + // Loads a language definition into the `languages` cache. The function + // takes a key and optionally values. If not in the browser and no values + // are provided, it will load the language file module. As a convenience, + // this function also returns the language values. + function loadLang(key, values) { + values.abbr = key; + if (!languages[key]) { + languages[key] = new Language(); + } + languages[key].set(values); + return languages[key]; + } + + // Determines which language definition to use and returns it. + // + // With no parameters, it will return the global language. If you + // pass in a language key, such as 'en', it will return the + // definition for 'en', so long as 'en' has already been loaded using + // moment.lang. + function getLangDefinition(key) { + if (!key) { + return moment.fn._lang; + } + if (!languages[key] && hasModule) { + require('./lang/' + key); + } + return languages[key]; + } + + + /************************************ + Formatting + ************************************/ + + + function removeFormattingTokens(input) { + if (input.match(/\[.*\]/)) { + return input.replace(/^\[|\]$/g, ""); + } + return input.replace(/\\/g, ""); + } + + function makeFormatFunction(format) { + var array = format.match(formattingTokens), i, length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = ""; + for (i = 0; i < length; i++) { + output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i]; + } + return output; + }; + } + + // format date using native date object + function formatMoment(m, format) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return m.lang().longDateFormat(input) || input; + } + + while (i-- && localFormattingTokens.test(format)) { + format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); + } + + if (!formatFunctions[format]) { + formatFunctions[format] = makeFormatFunction(format); + } + + return formatFunctions[format](m); + } + + + /************************************ + Parsing + ************************************/ + + + // get the regex to find the next token + function getParseRegexForToken(token) { + switch (token) { + case 'DDDD': + return parseTokenThreeDigits; + case 'YYYY': + return parseTokenFourDigits; + case 'YYYYY': + return parseTokenSixDigits; + case 'S': + case 'SS': + case 'SSS': + case 'DDD': + return parseTokenOneToThreeDigits; + case 'MMM': + case 'MMMM': + case 'dd': + case 'ddd': + case 'dddd': + case 'a': + case 'A': + return parseTokenWord; + case 'X': + return parseTokenTimestampMs; + case 'Z': + case 'ZZ': + return parseTokenTimezone; + case 'T': + return parseTokenT; + case 'MM': + case 'DD': + case 'YY': + case 'HH': + case 'hh': + case 'mm': + case 'ss': + case 'M': + case 'D': + case 'd': + case 'H': + case 'h': + case 'm': + case 's': + return parseTokenOneOrTwoDigits; + default : + return new RegExp(token.replace('\\', '')); + } + } + + // function to convert string input to date + function addTimeToArrayFromToken(token, input, config) { + var a, b, + datePartArray = config._a; + + switch (token) { + // MONTH + case 'M' : // fall through to MM + case 'MM' : + datePartArray[1] = (input == null) ? 0 : ~~input - 1; + break; + case 'MMM' : // fall through to MMMM + case 'MMMM' : + a = getLangDefinition(config._l).monthsParse(input); + // if we didn't find a month name, mark the date as invalid. + if (a != null) { + datePartArray[1] = a; + } else { + config._isValid = false; + } + break; + // DAY OF MONTH + case 'D' : // fall through to DDDD + case 'DD' : // fall through to DDDD + case 'DDD' : // fall through to DDDD + case 'DDDD' : + if (input != null) { + datePartArray[2] = ~~input; + } + break; + // YEAR + case 'YY' : + datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000); + break; + case 'YYYY' : + case 'YYYYY' : + datePartArray[0] = ~~input; + break; + // AM / PM + case 'a' : // fall through to A + case 'A' : + config._isPm = ((input + '').toLowerCase() === 'pm'); + break; + // 24 HOUR + case 'H' : // fall through to hh + case 'HH' : // fall through to hh + case 'h' : // fall through to hh + case 'hh' : + datePartArray[3] = ~~input; + break; + // MINUTE + case 'm' : // fall through to mm + case 'mm' : + datePartArray[4] = ~~input; + break; + // SECOND + case 's' : // fall through to ss + case 'ss' : + datePartArray[5] = ~~input; + break; + // MILLISECOND + case 'S' : + case 'SS' : + case 'SSS' : + datePartArray[6] = ~~ (('0.' + input) * 1000); + break; + // UNIX TIMESTAMP WITH MS + case 'X': + config._d = new Date(parseFloat(input) * 1000); + break; + // TIMEZONE + case 'Z' : // fall through to ZZ + case 'ZZ' : + config._useUTC = true; + a = (input + '').match(parseTimezoneChunker); + if (a && a[1]) { + config._tzh = ~~a[1]; + } + if (a && a[2]) { + config._tzm = ~~a[2]; + } + // reverse offsets + if (a && a[0] === '+') { + config._tzh = -config._tzh; + config._tzm = -config._tzm; + } + break; + } + + // if the input is null, the date is not valid + if (input == null) { + config._isValid = false; + } + } + + // convert an array to a date. + // the array should mirror the parameters below + // note: all values past the year are optional and will default to the lowest possible value. + // [year, month, day , hour, minute, second, millisecond] + function dateFromArray(config) { + var i, date, input = []; + + if (config._d) { + return; + } + + for (i = 0; i < 7; i++) { + config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // add the offsets to the time to be parsed so that we can have a clean array for checking isValid + input[3] += config._tzh || 0; + input[4] += config._tzm || 0; + + date = new Date(0); + + if (config._useUTC) { + date.setUTCFullYear(input[0], input[1], input[2]); + date.setUTCHours(input[3], input[4], input[5], input[6]); + } else { + date.setFullYear(input[0], input[1], input[2]); + date.setHours(input[3], input[4], input[5], input[6]); + } + + config._d = date; + } + + // date from string and format string + function makeDateFromStringAndFormat(config) { + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var tokens = config._f.match(formattingTokens), + string = config._i, + i, parsedInput; + + config._a = []; + + for (i = 0; i < tokens.length; i++) { + parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; + if (parsedInput) { + string = string.slice(string.indexOf(parsedInput) + parsedInput.length); + } + // don't parse if its not a known token + if (formatTokenFunctions[tokens[i]]) { + addTimeToArrayFromToken(tokens[i], parsedInput, config); + } + } + // handle am pm + if (config._isPm && config._a[3] < 12) { + config._a[3] += 12; + } + // if is 12 am, change hours to 0 + if (config._isPm === false && config._a[3] === 12) { + config._a[3] = 0; + } + // return + dateFromArray(config); + } + + // date from string and array of format strings + function makeDateFromStringAndArray(config) { + var tempConfig, + tempMoment, + bestMoment, + + scoreToBeat = 99, + i, + currentDate, + currentScore; + + while (config._f.length) { + tempConfig = extend({}, config); + tempConfig._f = config._f.pop(); + makeDateFromStringAndFormat(tempConfig); + tempMoment = new Moment(tempConfig); + + if (tempMoment.isValid()) { + bestMoment = tempMoment; + break; + } + + currentScore = compareArrays(tempConfig._a, tempMoment.toArray()); + + if (currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempMoment; + } + } + + extend(config, bestMoment); + } + + // date from iso format + function makeDateFromString(config) { + var i, + string = config._i; + if (isoRegex.exec(string)) { + config._f = 'YYYY-MM-DDT'; + for (i = 0; i < 4; i++) { + if (isoTimes[i][1].exec(string)) { + config._f += isoTimes[i][0]; + break; + } + } + if (parseTokenTimezone.exec(string)) { + config._f += " Z"; + } + makeDateFromStringAndFormat(config); + } else { + config._d = new Date(string); + } + } + + function makeDateFromInput(config) { + var input = config._i, + matched = aspNetJsonRegex.exec(input); + + if (input === undefined) { + config._d = new Date(); + } else if (matched) { + config._d = new Date(+matched[1]); + } else if (typeof input === 'string') { + makeDateFromString(config); + } else if (isArray(input)) { + config._a = input.slice(0); + dateFromArray(config); + } else { + config._d = input instanceof Date ? new Date(+input) : new Date(input); + } + } + + + /************************************ + Relative Time + ************************************/ + + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { + return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function relativeTime(milliseconds, withoutSuffix, lang) { + var seconds = round(Math.abs(milliseconds) / 1000), + minutes = round(seconds / 60), + hours = round(minutes / 60), + days = round(hours / 24), + years = round(days / 365), + args = seconds < 45 && ['s', seconds] || + minutes === 1 && ['m'] || + minutes < 45 && ['mm', minutes] || + hours === 1 && ['h'] || + hours < 22 && ['hh', hours] || + days === 1 && ['d'] || + days <= 25 && ['dd', days] || + days <= 45 && ['M'] || + days < 345 && ['MM', round(days / 30)] || + years === 1 && ['y'] || ['yy', years]; + args[2] = withoutSuffix; + args[3] = milliseconds > 0; + args[4] = lang; + return substituteTimeAgo.apply({}, args); + } + + + /************************************ + Week of Year + ************************************/ + + + // firstDayOfWeek 0 = sun, 6 = sat + // the day of the week that starts the week + // (usually sunday or monday) + // firstDayOfWeekOfYear 0 = sun, 6 = sat + // the first week is the week that contains the first + // of this day of the week + // (eg. ISO weeks use thursday (4)) + function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { + var end = firstDayOfWeekOfYear - firstDayOfWeek, + daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(); + + + if (daysToDayOfWeek > end) { + daysToDayOfWeek -= 7; + } + + if (daysToDayOfWeek < end - 7) { + daysToDayOfWeek += 7; + } + + return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7); + } + + + /************************************ + Top Level Functions + ************************************/ + + function makeMoment(config) { + var input = config._i, + format = config._f; + + if (input === null || input === '') { + return null; + } + + if (typeof input === 'string') { + config._i = input = getLangDefinition().preparse(input); + } + + if (moment.isMoment(input)) { + config = extend({}, input); + config._d = new Date(+input._d); + } else if (format) { + if (isArray(format)) { + makeDateFromStringAndArray(config); + } else { + makeDateFromStringAndFormat(config); + } + } else { + makeDateFromInput(config); + } + + return new Moment(config); + } + + moment = function (input, format, lang) { + return makeMoment({ + _i : input, + _f : format, + _l : lang, + _isUTC : false + }); + }; + + // creating with utc + moment.utc = function (input, format, lang) { + return makeMoment({ + _useUTC : true, + _isUTC : true, + _l : lang, + _i : input, + _f : format + }); + }; + + // creating with unix timestamp (in seconds) + moment.unix = function (input) { + return moment(input * 1000); + }; + + // duration + moment.duration = function (input, key) { + var isDuration = moment.isDuration(input), + isNumber = (typeof input === 'number'), + duration = (isDuration ? input._data : (isNumber ? {} : input)), + ret; + + if (isNumber) { + if (key) { + duration[key] = input; + } else { + duration.milliseconds = input; + } + } + + ret = new Duration(duration); + + if (isDuration && input.hasOwnProperty('_lang')) { + ret._lang = input._lang; + } + + return ret; + }; + + // version number + moment.version = VERSION; + + // default format + moment.defaultFormat = isoFormat; + + // This function will load languages and then set the global language. If + // no arguments are passed in, it will simply return the current global + // language key. + moment.lang = function (key, values) { + var i; + + if (!key) { + return moment.fn._lang._abbr; + } + if (values) { + loadLang(key, values); + } else if (!languages[key]) { + getLangDefinition(key); + } + moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); + }; + + // returns language data + moment.langData = function (key) { + if (key && key._lang && key._lang._abbr) { + key = key._lang._abbr; + } + return getLangDefinition(key); + }; + + // compare moment object + moment.isMoment = function (obj) { + return obj instanceof Moment; + }; + + // for typechecking Duration objects + moment.isDuration = function (obj) { + return obj instanceof Duration; + }; + + + /************************************ + Moment Prototype + ************************************/ + + + moment.fn = Moment.prototype = { + + clone : function () { + return moment(this); + }, + + valueOf : function () { + return +this._d; + }, + + unix : function () { + return Math.floor(+this._d / 1000); + }, + + toString : function () { + return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); + }, + + toDate : function () { + return this._d; + }, + + toJSON : function () { + return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + }, + + toArray : function () { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hours(), + m.minutes(), + m.seconds(), + m.milliseconds() + ]; + }, + + isValid : function () { + if (this._isValid == null) { + if (this._a) { + this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()); + } else { + this._isValid = !isNaN(this._d.getTime()); + } + } + return !!this._isValid; + }, + + utc : function () { + this._isUTC = true; + return this; + }, + + local : function () { + this._isUTC = false; + return this; + }, + + format : function (inputString) { + var output = formatMoment(this, inputString || moment.defaultFormat); + return this.lang().postformat(output); + }, + + add : function (input, val) { + var dur; + // switch args to support add('s', 1) and add(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, 1); + return this; + }, + + subtract : function (input, val) { + var dur; + // switch args to support subtract('s', 1) and subtract(1, 's') + if (typeof input === 'string') { + dur = moment.duration(+val, input); + } else { + dur = moment.duration(input, val); + } + addOrSubtractDurationFromMoment(this, dur, -1); + return this; + }, + + diff : function (input, units, asFloat) { + var that = this._isUTC ? moment(input).utc() : moment(input).local(), + zoneDiff = (this.zone() - that.zone()) * 6e4, + diff, output; + + if (units) { + // standardize on singular form + units = units.replace(/s$/, ''); + } + + if (units === 'year' || units === 'month') { + diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 + output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); + output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff; + if (units === 'year') { + output = output / 12; + } + } else { + diff = (this - that) - zoneDiff; + output = units === 'second' ? diff / 1e3 : // 1000 + units === 'minute' ? diff / 6e4 : // 1000 * 60 + units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 + units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24 + units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 + diff; + } + return asFloat ? output : absRound(output); + }, + + from : function (time, withoutSuffix) { + return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); + }, + + fromNow : function (withoutSuffix) { + return this.from(moment(), withoutSuffix); + }, + + calendar : function () { + var diff = this.diff(moment().startOf('day'), 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + return this.format(this.lang().calendar(format, this)); + }, + + isLeapYear : function () { + var year = this.year(); + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + }, + + isDST : function () { + return (this.zone() < moment([this.year()]).zone() || + this.zone() < moment([this.year(), 5]).zone()); + }, + + day : function (input) { + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + return input == null ? day : + this.add({ d : input - day }); + }, + + startOf: function (units) { + units = units.replace(/s$/, ''); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + /* falls through */ + } + + // weeks are a special case + if (units === 'week') { + this.day(0); + } + + return this; + }, + + endOf: function (units) { + return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1); + }, + + isAfter: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) > +moment(input).startOf(units); + }, + + isBefore: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) < +moment(input).startOf(units); + }, + + isSame: function (input, units) { + units = typeof units !== 'undefined' ? units : 'millisecond'; + return +this.clone().startOf(units) === +moment(input).startOf(units); + }, + + zone : function () { + return this._isUTC ? 0 : this._d.getTimezoneOffset(); + }, + + daysInMonth : function () { + return moment.utc([this.year(), this.month() + 1, 0]).date(); + }, + + dayOfYear : function (input) { + var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); + }, + + isoWeek : function (input) { + var week = weekOfYear(this, 1, 4); + return input == null ? week : this.add("d", (input - week) * 7); + }, + + week : function (input) { + var week = this.lang().week(this); + return input == null ? week : this.add("d", (input - week) * 7); + }, + + // If passed a language key, it will set the language for this + // instance. Otherwise, it will return the language configuration + // variables for this instance. + lang : function (key) { + if (key === undefined) { + return this._lang; + } else { + this._lang = getLangDefinition(key); + return this; + } + } + }; + + // helper for adding shortcuts + function makeGetterAndSetter(name, key) { + moment.fn[name] = moment.fn[name + 's'] = function (input) { + var utc = this._isUTC ? 'UTC' : ''; + if (input != null) { + this._d['set' + utc + key](input); + return this; + } else { + return this._d['get' + utc + key](); + } + }; + } + + // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) + for (i = 0; i < proxyGettersAndSetters.length; i ++) { + makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); + } + + // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') + makeGetterAndSetter('year', 'FullYear'); + + // add plural methods + moment.fn.days = moment.fn.day; + moment.fn.weeks = moment.fn.week; + moment.fn.isoWeeks = moment.fn.isoWeek; + + /************************************ + Duration Prototype + ************************************/ + + + moment.duration.fn = Duration.prototype = { + weeks : function () { + return absRound(this.days() / 7); + }, + + valueOf : function () { + return this._milliseconds + + this._days * 864e5 + + this._months * 2592e6; + }, + + humanize : function (withSuffix) { + var difference = +this, + output = relativeTime(difference, !withSuffix, this.lang()); + + if (withSuffix) { + output = this.lang().pastFuture(difference, output); + } + + return this.lang().postformat(output); + }, + + lang : moment.fn.lang + }; + + function makeDurationGetter(name) { + moment.duration.fn[name] = function () { + return this._data[name]; + }; + } + + function makeDurationAsGetter(name, factor) { + moment.duration.fn['as' + name] = function () { + return +this / factor; + }; + } + + for (i in unitMillisecondFactors) { + if (unitMillisecondFactors.hasOwnProperty(i)) { + makeDurationAsGetter(i, unitMillisecondFactors[i]); + makeDurationGetter(i.toLowerCase()); + } + } + + makeDurationAsGetter('Weeks', 6048e5); + + + /************************************ + Default Lang + ************************************/ + + + // Set default language, other languages will inherit from English. + moment.lang('en', { + ordinal : function (number) { + var b = number % 10, + output = (~~ (number % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return number + output; + } + }); + + + /************************************ + Exposing Moment + ************************************/ + + + // CommonJS module is defined + if (hasModule) { + module.exports = moment; + } + /*global ender:false */ + if (typeof ender === 'undefined') { + // here, `this` means `window` in the browser, or `global` on the server + // add `moment` as a global object via a string identifier, + // for Closure Compiler "advanced" mode + this['moment'] = moment; + } + /*global define:false */ + if (typeof define === "function" && define.amd) { + define("moment", [], function () { + return moment; + }); + } +}).call(this); From 9514c46aa048ee7357b7e3f5d105fa8c572dc7c0 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 16:30:25 +0100 Subject: [PATCH 13/74] Date parsing and toISOString() can't be assumed to be present. Use Moment instead. Issue #323. --- src/backend.memory.js | 2 +- test/view.timeline.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend.memory.js b/src/backend.memory.js index 861a0636..897d1baf 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -109,7 +109,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, string : function (e) { return e.toString() }, - date : function (e) { return new Date(e).valueOf() }, + date : function (e) { return moment(e).valueOf() }, datetime : function (e) { return new Date(e).valueOf() } }; var keyedFields = {}; diff --git a/test/view.timeline.test.js b/test/view.timeline.test.js index 83b4a60f..0b6637d8 100644 --- a/test/view.timeline.test.js +++ b/test/view.timeline.test.js @@ -67,7 +67,7 @@ test('_parseDate', function () { ]; _.each(testData, function(item) { var out = view._parseDate(item[0]); - if (out) out = out.toISOString(); + if (out) out = moment(out).toJSON(); equal(out, item[1]); }); }); From cf700f4ac0d04726b0aaa812ecfa7ce8db925d4c Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 17:20:16 +0100 Subject: [PATCH 14/74] Bumped qunit and sinon. Added qunit-assert-html. --- test/backend.elasticsearch.test.js | 2 - test/built.html | 3 +- test/index.html | 3 +- test/qunit/qunit-assert-html.js | 377 +++ test/qunit/qunit.css | 64 +- test/qunit/qunit.js | 1007 +++++-- test/sinon/1.1.1/sinon.js | 2820 ------------------ test/sinon/1.7.1/sinon.js | 4299 ++++++++++++++++++++++++++++ 8 files changed, 5416 insertions(+), 3159 deletions(-) create mode 100644 test/qunit/qunit-assert-html.js delete mode 100644 test/sinon/1.1.1/sinon.js create mode 100644 test/sinon/1.7.1/sinon.js diff --git a/test/backend.elasticsearch.test.js b/test/backend.elasticsearch.test.js index 18a05998..c97bc744 100644 --- a/test/backend.elasticsearch.test.js +++ b/test/backend.elasticsearch.test.js @@ -191,7 +191,6 @@ test("query", function() { equal(3, queryResult.hits.total); equal(3, queryResult.hits.hits.length); equal('Note 1', queryResult.hits.hits[0]._source['title']); - start(); }); $.ajax.restore(); }); @@ -284,7 +283,6 @@ test("query", function() { equal(3, dataset.recordCount); equal(3, recList.length); equal('Note 1', recList.models[0].get('title')); - start(); }); }); $.ajax.restore(); diff --git a/test/built.html b/test/built.html index 4e6c04af..3e88af2c 100644 --- a/test/built.html +++ b/test/built.html @@ -18,7 +18,8 @@ - + + diff --git a/test/index.html b/test/index.html index 5d4f44ea..f51ccd39 100644 --- a/test/index.html +++ b/test/index.html @@ -25,7 +25,8 @@ - + + diff --git a/test/qunit/qunit-assert-html.js b/test/qunit/qunit-assert-html.js new file mode 100644 index 00000000..223a0fec --- /dev/null +++ b/test/qunit/qunit-assert-html.js @@ -0,0 +1,377 @@ +/*global QUnit:false */ +(function( QUnit, window, undefined ) { + "use strict"; + + var trim = function( s ) { + if ( !s ) { + return ""; + } + return typeof s.trim === "function" ? s.trim() : s.replace( /^\s+|\s+$/g, "" ); + }; + + var normalizeWhitespace = function( s ) { + if ( !s ) { + return ""; + } + return trim( s.replace( /\s+/g, " " ) ); + }; + + var dedupeFlatDict = function( dictToDedupe, parentDict ) { + var key, val; + if ( parentDict ) { + for ( key in dictToDedupe ) { + val = dictToDedupe[key]; + if ( val && ( val === parentDict[key] ) ) { + delete dictToDedupe[key]; + } + } + } + return dictToDedupe; + }; + + var objectKeys = Object.keys || (function() { + var hasOwn = function( obj, propName ) { + return Object.prototype.hasOwnProperty.call( obj, propName ); + }; + return function( obj ) { + var keys = [], + key; + for ( key in obj ) { + if ( hasOwn( obj, key ) ) { + keys.push( key ); + } + } + return keys; + }; + })(); + + /** + * Calculate based on `currentStyle`/`getComputedStyle` styles instead + */ + var getElementStyles = (function() { + + // Memoized + var camelCase = (function() { + var camelCaseFn = (function() { + // Matches dashed string for camelizing + var rmsPrefix = /^-ms-/, + msPrefixFix = "ms-", + rdashAlpha = /-([\da-z])/gi, + camelCaseReplacerFn = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }; + + return function( s ) { + return s.replace(rmsPrefix, msPrefixFix).replace(rdashAlpha, camelCaseReplacerFn); + }; + })(); + + var camelCaseMemoizer = {}; + + return function( s ) { + var temp = camelCaseMemoizer[s]; + if ( temp ) { + return temp; + } + + temp = camelCaseFn( s ); + camelCaseMemoizer[s] = temp; + return temp; + }; + })(); + + var styleKeySortingFn = function( a, b ) { + return camelCase( a ) < camelCase( b ); + }; + + return function( elem ) { + var styleCount, i, key, + styles = {}, + styleKeys = [], + style = elem.ownerDocument.defaultView ? + elem.ownerDocument.defaultView.getComputedStyle( elem, null ) : + elem.currentStyle; + + // `getComputedStyle` + if ( style && style.length && style[0] && style[style[0]] ) { + styleCount = style.length; + while ( styleCount-- ) { + styleKeys.push( style[styleCount] ); + } + styleKeys.sort( styleKeySortingFn ); + + for ( i = 0, styleCount = styleKeys.length ; i < styleCount ; i++ ) { + key = styleKeys[i]; + if ( key !== "cssText" && typeof style[key] === "string" && style[key] ) { + styles[camelCase( key )] = style[key]; + } + } + } + // `currentStyle` support: IE < 9.0, Opera < 10.6 + else { + for ( key in style ) { + styleKeys.push( key ); + } + styleKeys.sort(); + + for ( i = 0, styleCount = styleKeys.length ; i < styleCount ; i++ ) { + key = styleKeys[i]; + if ( key !== "cssText" && typeof style[key] === "string" && style[key] ) { + styles[key] = style[key]; + } + } + } + + return styles; + + }; + })(); + + var serializeElementNode = function( elementNode, rootNodeStyles ) { + var subNodes, i, len, styles, attrName, + serializedNode = { + NodeType: elementNode.nodeType, + NodeName: elementNode.nodeName.toLowerCase(), + Attributes: {}, + ChildNodes: [] + }; + + subNodes = elementNode.attributes; + for ( i = 0, len = subNodes.length ; i < len ; i++ ) { + attrName = subNodes[i].name.toLowerCase(); + if ( attrName === "class" ) { + serializedNode.Attributes[attrName] = normalizeWhitespace( subNodes[i].value ); + } + else if ( attrName !== "style" ) { + serializedNode.Attributes[attrName] = subNodes[i].value; + } + // Ignore the "style" attribute completely + } + + // Only add the style attribute if there is 1+ pertinent rules + styles = dedupeFlatDict( getElementStyles( elementNode ), rootNodeStyles ); + if ( styles && objectKeys( styles ).length ) { + serializedNode.Attributes["style"] = styles; + } + + subNodes = elementNode.childNodes; + for ( i = 0, len = subNodes.length; i < len; i++ ) { + serializedNode.ChildNodes.push( serializeNode( subNodes[i], rootNodeStyles ) ); + } + + return serializedNode; + }; + + var serializeNode = function( node, rootNodeStyles ) { + var serializedNode; + + switch (node.nodeType) { + case 1: // Node.ELEMENT_NODE + serializedNode = serializeElementNode( node, rootNodeStyles ); + break; + case 3: // Node.TEXT_NODE + serializedNode = { + NodeType: node.nodeType, + NodeName: node.nodeName.toLowerCase(), + NodeValue: node.nodeValue + }; + break; + case 4: // Node.CDATA_SECTION_NODE + case 7: // Node.PROCESSING_INSTRUCTION_NODE + case 8: // Node.COMMENT_NODE + serializedNode = { + NodeType: node.nodeType, + NodeName: node.nodeName.toLowerCase(), + NodeValue: trim( node.nodeValue ) + }; + break; + case 5: // Node.ENTITY_REFERENCE_NODE + case 6: // Node.ENTITY_NODE + case 9: // Node.DOCUMENT_NODE + case 10: // Node.DOCUMENT_TYPE_NODE + case 11: // Node.DOCUMENT_FRAGMENT_NODE + case 12: // Node.NOTATION_NODE + serializedNode = { + NodeType: node.nodeType, + NodeName: node.nodeName + }; + break; + case 2: // Node.ATTRIBUTE_NODE + throw new Error( "`node.nodeType` was `Node.ATTRIBUTE_NODE` (2), which is not supported by this method" ); + default: + throw new Error( "`node.nodeType` was not recognized: " + node.nodeType ); + } + + return serializedNode; + }; + + var serializeHtml = function( html ) { + var scratch = getCleanSlate(), + rootNode = scratch.container(), + rootNodeStyles = getElementStyles( rootNode ), + serializedHtml = [], + kids, i, len; + rootNode.innerHTML = trim( html ); + + kids = rootNode.childNodes; + for ( i = 0, len = kids.length; i < len; i++ ) { + serializedHtml.push( serializeNode( kids[i], rootNodeStyles ) ); + } + + scratch.reset(); + + return serializedHtml; + }; + + var getCleanSlate = (function() { + var containerElId = "qunit-html-addon-container", + iframeReady = false, + iframeLoaded = function() { + iframeReady = true; + }, + iframeReadied = function() { + if (iframe.readyState === "complete" || iframe.readyState === 4) { + iframeReady = true; + } + }, + iframeApi, + iframe, + iframeWin, + iframeDoc; + + if ( !iframeApi ) { + + QUnit.begin(function() { + // Initialize the background iframe! + if ( !iframe || !iframeWin || !iframeDoc ) { + iframe = window.document.createElement( "iframe" ); + QUnit.addEvent( iframe, "load", iframeLoaded ); + QUnit.addEvent( iframe, "readystatechange", iframeReadied ); + iframe.style.position = "absolute"; + iframe.style.top = iframe.style.left = "-1000px"; + iframe.height = iframe.width = 0; + + // `getComputedStyle` behaves inconsistently cross-browser when not attached to a live DOM + window.document.body.appendChild( iframe ); + + iframeWin = iframe.contentWindow || + iframe.window || + iframe.contentDocument && iframe.contentDocument.defaultView || + iframe.document && ( iframe.document.defaultView || iframe.document.window ) || + window.frames[( iframe.name || iframe.id )]; + + iframeDoc = iframeWin && iframeWin.document || + iframe.contentDocument || + iframe.document; + + var iframeContents = [ + "", + "", + "", + " QUnit HTML addon iframe", + "", + "", + "
", + " ", + "", + "" + ].join( "\n" ); + + iframeDoc.open(); + iframeDoc.write( iframeContents ); + iframeDoc.close(); + + // Is ready? + iframeReady = iframeReady || iframeWin.isReady; + } + }); + + QUnit.done(function() { + if ( iframe && iframe.ownerDocument ) { + iframe.parentNode.removeChild( iframe ); + } + iframe = iframeWin = iframeDoc = null; + iframeReady = false; + }); + + var waitForIframeReady = function( maxTimeout ) { + if ( !iframeReady ) { + if ( !maxTimeout ) { + maxTimeout = 2000; // 2 seconds MAX + } + var startTime = new Date(); + while ( !iframeReady && ( ( new Date() - startTime ) < maxTimeout ) ) { + iframeReady = iframeReady || iframeWin.isReady; + } + } + }; + + iframeApi = { + container: function() { + waitForIframeReady(); + if ( iframeReady && iframeDoc ) { + return iframeDoc.getElementById( containerElId ); + } + return undefined; + }, + reset: function() { + var containerEl = iframeApi.container(); + if ( containerEl ) { + containerEl.innerHTML = ""; + } + } + }; + } + + // Actual function signature for `getCleanState` + return function() { return iframeApi; }; + })(); + + QUnit.extend( QUnit.assert, { + + /** + * Compare two snippets of HTML for equality after normalization. + * + * @example assert.htmlEqual("Hello, QUnit! ", "Hello, QUnit!", "HTML should be equal"); + * @param {String} actual The actual HTML before normalization. + * @param {String} expected The excepted HTML before normalization. + * @param {String} [message] Optional message to display in the results. + */ + htmlEqual: function( actual, expected, message ) { + if ( !message ) { + message = "HTML should be equal"; + } + + this.deepEqual( serializeHtml( actual ), serializeHtml( expected ), message ); + }, + + /** + * Compare two snippets of HTML for inequality after normalization. + * + * @example assert.notHtmlEqual("Hello, QUnit!", "Hello, QUnit!", "HTML should not be equal"); + * @param {String} actual The actual HTML before normalization. + * @param {String} expected The excepted HTML before normalization. + * @param {String} [message] Optional message to display in the results. + */ + notHtmlEqual: function( actual, expected, message ) { + if ( !message ) { + message = "HTML should not be equal"; + } + + this.notDeepEqual( serializeHtml( actual ), serializeHtml( expected ), message ); + }, + + /** + * @private + * Normalize and serialize an HTML snippet. Primarily only exposed for unit testing purposes. + * + * @example assert._serializeHtml('Test'); + * @param {String} html The HTML snippet to normalize and serialize. + * @returns {Object[]} The normalized and serialized form of the HTML snippet. + */ + _serializeHtml: serializeHtml + + }); +})( QUnit, this ); diff --git a/test/qunit/qunit.css b/test/qunit/qunit.css index 23235ec8..d7fc0c8e 100644 --- a/test/qunit/qunit.css +++ b/test/qunit/qunit.css @@ -1,11 +1,11 @@ /** - * QUnit v1.6.0 - A JavaScript Unit Testing Framework + * QUnit v1.11.0 - A JavaScript Unit Testing Framework * - * http://docs.jquery.com/QUnit + * http://qunitjs.com * - * Copyright (c) 2012 John Resig, Jörn Zaefferer - * Dual licensed under the MIT (MIT-LICENSE.txt) - * or GPL (GPL-LICENSE.txt) licenses. + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license */ /** Font Family and Sizes */ @@ -20,7 +20,7 @@ /** Resets */ -#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { +#qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { margin: 0; padding: 0; } @@ -38,10 +38,10 @@ line-height: 1em; font-weight: normal; - border-radius: 15px 15px 0 0; - -moz-border-radius: 15px 15px 0 0; - -webkit-border-top-right-radius: 15px; - -webkit-border-top-left-radius: 15px; + border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + -webkit-border-top-right-radius: 5px; + -webkit-border-top-left-radius: 5px; } #qunit-header a { @@ -54,9 +54,9 @@ color: #fff; } -#qunit-header label { +#qunit-testrunner-toolbar label { display: inline-block; - padding-left: 0.5em; + padding: 0 .5em 0 .1em; } #qunit-banner { @@ -67,6 +67,7 @@ padding: 0.5em 0 0.5em 2em; color: #5E740B; background-color: #eee; + overflow: hidden; } #qunit-userAgent { @@ -76,6 +77,9 @@ text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } +#qunit-modulefilter-container { + float: right; +} /** Tests: Pass/Fail */ @@ -107,19 +111,24 @@ color: #000; } -#qunit-tests ol { +#qunit-tests li .runtime { + float: right; + font-size: smaller; +} + +.qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #fff; - border-radius: 15px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; + border-radius: 5px; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; +} - box-shadow: inset 0px 2px 13px #999; - -moz-box-shadow: inset 0px 2px 13px #999; - -webkit-box-shadow: inset 0px 2px 13px #999; +.qunit-collapsed { + display: none; } #qunit-tests table { @@ -162,8 +171,7 @@ #qunit-tests b.failed { color: #710909; } #qunit-tests li li { - margin: 0.5em; - padding: 0.4em 0.5em 0.4em 0.5em; + padding: 5px; background-color: #fff; border-bottom: none; list-style-position: inside; @@ -172,9 +180,9 @@ /*** Passing Styles */ #qunit-tests li li.pass { - color: #5E740B; + color: #3c510c; background-color: #fff; - border-left: 26px solid #C6E746; + border-left: 10px solid #C6E746; } #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } @@ -190,15 +198,15 @@ #qunit-tests li li.fail { color: #710909; background-color: #fff; - border-left: 26px solid #EE5757; + border-left: 10px solid #EE5757; white-space: pre; } #qunit-tests > li:last-child { - border-radius: 0 0 15px 15px; - -moz-border-radius: 0 0 15px 15px; - -webkit-border-bottom-right-radius: 15px; - -webkit-border-bottom-left-radius: 15px; + border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; } #qunit-tests .fail { color: #000000; background-color: #EE5757; } diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js index 2c277fab..302545f4 100644 --- a/test/qunit/qunit.js +++ b/test/qunit/qunit.js @@ -1,54 +1,113 @@ /** - * QUnit v1.6.0 - A JavaScript Unit Testing Framework + * QUnit v1.11.0 - A JavaScript Unit Testing Framework * - * http://docs.jquery.com/QUnit + * http://qunitjs.com * - * Copyright (c) 2012 John Resig, Jörn Zaefferer - * Dual licensed under the MIT (MIT-LICENSE.txt) - * or GPL (GPL-LICENSE.txt) licenses. + * Copyright 2012 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license */ (function( window ) { var QUnit, + assert, config, + onErrorFnPrev, testId = 0, + fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), toString = Object.prototype.toString, hasOwn = Object.prototype.hasOwnProperty, + // Keep a local reference to Date (GH-283) + Date = window.Date, defined = { - setTimeout: typeof window.setTimeout !== "undefined", - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch( e ) { - return false; + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch( e ) { + return false; + } + }()) + }, + /** + * Provides a normalized error string, correcting an issue + * with IE 7 (and prior) where Error.prototype.toString is + * not properly implemented + * + * Based on http://es5.github.com/#x15.11.4.4 + * + * @param {String|Error} error + * @return {String} error message + */ + errorString = function( error ) { + var name, message, + errorString = error.toString(); + if ( errorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return errorString; } - }()) -}; + }, + /** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ + objectValues = function( obj ) { + // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. + /*jshint newcap: false */ + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[key]; + vals[key] = val === Object(val) ? objectValues(val) : val; + } + } + return vals; + }; -function Test( name, testName, expected, async, callback ) { - this.name = name; - this.testName = testName; - this.expected = expected; - this.async = async; - this.callback = callback; +function Test( settings ) { + extend( this, settings ); this.assertions = []; + this.testNumber = ++Test.count; } +Test.count = 0; + Test.prototype = { init: function() { - var b, li, - tests = id( "qunit-tests" ); + var a, b, li, + tests = id( "qunit-tests" ); if ( tests ) { b = document.createElement( "strong" ); - b.innerHTML = "Running " + this.name; + b.innerHTML = this.nameHtml; + + // `a` initialized at top of scope + a = document.createElement( "a" ); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ testNumber: this.testNumber }); li = document.createElement( "li" ); li.appendChild( b ); + li.appendChild( a ); li.className = "running"; li.id = this.id = "qunit-test-output" + testId++; @@ -83,6 +142,7 @@ Test.prototype = { teardown: function() {} }, this.moduleTestEnvironment ); + this.started = +new Date(); runLoggingCallbacks( "testStart", QUnit, { name: this.testName, module: this.module @@ -102,7 +162,7 @@ Test.prototype = { try { this.testEnvironment.setup.call( this.testEnvironment ); } catch( e ) { - QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); + QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); } }, run: function() { @@ -111,22 +171,28 @@ Test.prototype = { var running = id( "qunit-testresult" ); if ( running ) { - running.innerHTML = "Running:
" + this.name; + running.innerHTML = "Running:
" + this.nameHtml; } if ( this.async ) { QUnit.stop(); } + this.callbackStarted = +new Date(); + if ( config.notrycatch ) { - this.callback.call( this.testEnvironment ); + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; return; } try { - this.callback.call( this.testEnvironment ); + this.callback.call( this.testEnvironment, QUnit.assert ); + this.callbackRuntime = +new Date() - this.callbackStarted; } catch( e ) { - QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + ": " + e.message, extractStacktrace( e, 1 ) ); + this.callbackRuntime = +new Date() - this.callbackStarted; + + QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); // else next test will carry the responsibility saveGlobal(); @@ -139,35 +205,43 @@ Test.prototype = { teardown: function() { config.current = this; if ( config.notrycatch ) { + if ( typeof this.callbackRuntime === "undefined" ) { + this.callbackRuntime = +new Date() - this.callbackStarted; + } this.testEnvironment.teardown.call( this.testEnvironment ); return; } else { try { this.testEnvironment.teardown.call( this.testEnvironment ); } catch( e ) { - QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); + QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); } } checkPollution(); }, finish: function() { config.current = this; - if ( this.expected != null && this.expected != this.assertions.length ) { + if ( config.requireExpects && this.expected === null ) { + QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); + } else if ( this.expected !== null && this.expected !== this.assertions.length ) { QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); - } else if ( this.expected == null && !this.assertions.length ) { + } else if ( this.expected === null && !this.assertions.length ) { QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); } - var assertion, a, b, i, li, ol, + var i, assertion, a, b, time, li, ol, + test = this, good = 0, bad = 0, tests = id( "qunit-tests" ); + this.runtime = +new Date() - this.started; config.stats.all += this.assertions.length; config.moduleStats.all += this.assertions.length; if ( tests ) { ol = document.createElement( "ol" ); + ol.className = "qunit-assert-list"; for ( i = 0; i < this.assertions.length; i++ ) { assertion = this.assertions[i]; @@ -196,42 +270,42 @@ Test.prototype = { } if ( bad === 0 ) { - ol.style.display = "none"; + addClass( ol, "qunit-collapsed" ); } // `b` initialized at top of scope b = document.createElement( "strong" ); - b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; - - // `a` initialized at top of scope - a = document.createElement( "a" ); - a.innerHTML = "Rerun"; - a.href = QUnit.url({ filter: getText([b]).replace( /\([^)]+\)$/, "" ).replace( /(^\s*|\s*$)/g, "" ) }); + b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; addEvent(b, "click", function() { - var next = b.nextSibling.nextSibling, - display = next.style.display; - next.style.display = display === "none" ? "block" : "none"; + var next = b.parentNode.lastChild, + collapsed = hasClass( next, "qunit-collapsed" ); + ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); }); addEvent(b, "dblclick", function( e ) { var target = e && e.target ? e.target : window.event.srcElement; - if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { + if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { target = target.parentNode; } if ( window.location && target.nodeName.toLowerCase() === "strong" ) { - window.location = QUnit.url({ - filter: getText([target]).replace( /\([^)]+\)$/, "" ).replace( /(^\s*|\s*$)/g, "" ) - }); + window.location = QUnit.url({ testNumber: test.testNumber }); } }); + // `time` initialized at top of scope + time = document.createElement( "span" ); + time.className = "runtime"; + time.innerHTML = this.runtime + " ms"; + // `li` initialized at top of scope li = id( this.id ); li.className = bad ? "fail" : "pass"; li.removeChild( li.firstChild ); + a = li.firstChild; li.appendChild( b ); li.appendChild( a ); + li.appendChild( time ); li.appendChild( ol ); } else { @@ -249,10 +323,13 @@ Test.prototype = { module: this.module, failed: bad, passed: this.assertions.length - bad, - total: this.assertions.length + total: this.assertions.length, + duration: this.runtime }); QUnit.reset(); + + config.current = undefined; }, queue: function() { @@ -291,13 +368,15 @@ Test.prototype = { } }; +// Root QUnit object. // `QUnit` initialized at top of scope QUnit = { // call on start of module test to prepend name to all tests module: function( name, testEnvironment ) { config.currentModule = name; - config.currentModuleTestEnviroment = testEnvironment; + config.currentModuleTestEnvironment = testEnvironment; + config.modules[name] = true; }, asyncTest: function( testName, expected, callback ) { @@ -311,7 +390,7 @@ QUnit = { test: function( testName, expected, callback, async ) { var test, - name = "" + escapeInnerText( testName ) + ""; + nameHtml = "" + escapeText( testName ) + ""; if ( arguments.length === 2 ) { callback = expected; @@ -319,117 +398,49 @@ QUnit = { } if ( config.currentModule ) { - name = "" + config.currentModule + ": " + name; + nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; } - if ( !validTest(config.currentModule + ": " + testName) ) { + test = new Test({ + nameHtml: nameHtml, + testName: testName, + expected: expected, + async: async, + callback: callback, + module: config.currentModule, + moduleTestEnvironment: config.currentModuleTestEnvironment, + stack: sourceFromStacktrace( 2 ) + }); + + if ( !validTest( test ) ) { return; } - test = new Test( name, testName, expected, async, callback ); - test.module = config.currentModule; - test.moduleTestEnvironment = config.currentModuleTestEnviroment; - test.stack = sourceFromStacktrace( 2 ); test.queue(); }, // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. expect: function( asserts ) { - config.current.expected = asserts; - }, - - // Asserts true. - // @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); - ok: function( result, msg ) { - if ( !config.current ) { - throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + if (arguments.length === 1) { + config.current.expected = asserts; + } else { + return config.current.expected; } - result = !!result; - - var source, - details = { - result: result, - message: msg - }; - - msg = escapeInnerText( msg || (result ? "okay" : "failed" ) ); - msg = "" + msg + ""; - - if ( !result ) { - source = sourceFromStacktrace( 2 ); - if ( source ) { - details.source = source; - msg += "
Source:
" + escapeInnerText( source ) + "
"; - } - } - runLoggingCallbacks( "log", QUnit, details ); - config.current.assertions.push({ - result: result, - message: msg - }); - }, - - // Checks that the first two arguments are equal, with an optional message. Prints out both actual and expected values. - // @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes." ); - equal: function( actual, expected, message ) { - QUnit.push( expected == actual, actual, expected, message ); - }, - - notEqual: function( actual, expected, message ) { - QUnit.push( expected != actual, actual, expected, message ); - }, - - deepEqual: function( actual, expected, message ) { - QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); - }, - - notDeepEqual: function( actual, expected, message ) { - QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); - }, - - strictEqual: function( actual, expected, message ) { - QUnit.push( expected === actual, actual, expected, message ); - }, - - notStrictEqual: function( actual, expected, message ) { - QUnit.push( expected !== actual, actual, expected, message ); - }, - - raises: function( block, expected, message ) { - var actual, - ok = false; - - if ( typeof expected === "string" ) { - message = expected; - expected = null; - } - - try { - block.call( config.current.testEnvironment ); - } catch (e) { - actual = e; - } - - if ( actual ) { - // we don't want to validate thrown error - if ( !expected ) { - ok = true; - // expected is a regexp - } else if ( QUnit.objectType( expected ) === "regexp" ) { - ok = expected.test( actual ); - // expected is a constructor - } else if ( actual instanceof expected ) { - ok = true; - // expected is a validation function which returns true is validation passed - } else if ( expected.call( {}, actual ) === true ) { - ok = true; - } - } - - QUnit.ok( ok, message ); }, start: function( count ) { + // QUnit hasn't been initialized yet. + // Note: RequireJS (et al) may delay onLoad + if ( config.semaphore === undefined ) { + QUnit.begin(function() { + // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first + setTimeout(function() { + QUnit.start( count ); + }); + }); + return; + } + config.semaphore -= count || 1; // don't start until equal number of stop-calls if ( config.semaphore > 0 ) { @@ -438,6 +449,8 @@ QUnit = { // ignore if start is called more often then stop if ( config.semaphore < 0 ) { config.semaphore = 0; + QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); + return; } // A slight delay, to avoid any current callbacks if ( defined.setTimeout ) { @@ -473,6 +486,191 @@ QUnit = { } }; +// `assert` initialized at top of scope +// Asssert helpers +// All of these must either call QUnit.push() or manually do: +// - runLoggingCallbacks( "log", .. ); +// - config.current.assertions.push({ .. }); +// We attach it to the QUnit object *after* we expose the public API, +// otherwise `assert` will become a global variable in browsers (#341). +assert = { + /** + * Asserts rough true-ish result. + * @name ok + * @function + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function( result, msg ) { + if ( !config.current ) { + throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + result = !!result; + + var source, + details = { + module: config.current.module, + name: config.current.testName, + result: result, + message: msg + }; + + msg = escapeText( msg || (result ? "okay" : "failed" ) ); + msg = "" + msg + ""; + + if ( !result ) { + source = sourceFromStacktrace( 2 ); + if ( source ) { + details.source = source; + msg += "
Source:
" + escapeText( source ) + "
"; + } + } + runLoggingCallbacks( "log", QUnit, details ); + config.current.assertions.push({ + result: result, + message: msg + }); + }, + + /** + * Assert that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * @name equal + * @function + * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); + */ + equal: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected == actual, actual, expected, message ); + }, + + /** + * @name notEqual + * @function + */ + notEqual: function( actual, expected, message ) { + /*jshint eqeqeq:false */ + QUnit.push( expected != actual, actual, expected, message ); + }, + + /** + * @name propEqual + * @function + */ + propEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notPropEqual + * @function + */ + notPropEqual: function( actual, expected, message ) { + actual = objectValues(actual); + expected = objectValues(expected); + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name deepEqual + * @function + */ + deepEqual: function( actual, expected, message ) { + QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name notDeepEqual + * @function + */ + notDeepEqual: function( actual, expected, message ) { + QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); + }, + + /** + * @name strictEqual + * @function + */ + strictEqual: function( actual, expected, message ) { + QUnit.push( expected === actual, actual, expected, message ); + }, + + /** + * @name notStrictEqual + * @function + */ + notStrictEqual: function( actual, expected, message ) { + QUnit.push( expected !== actual, actual, expected, message ); + }, + + "throws": function( block, expected, message ) { + var actual, + expectedOutput = expected, + ok = false; + + // 'expected' is optional + if ( typeof expected === "string" ) { + message = expected; + expected = null; + } + + config.current.ignoreGlobalErrors = true; + try { + block.call( config.current.testEnvironment ); + } catch (e) { + actual = e; + } + config.current.ignoreGlobalErrors = false; + + if ( actual ) { + // we don't want to validate thrown error + if ( !expected ) { + ok = true; + expectedOutput = null; + // expected is a regexp + } else if ( QUnit.objectType( expected ) === "regexp" ) { + ok = expected.test( errorString( actual ) ); + // expected is a constructor + } else if ( actual instanceof expected ) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if ( expected.call( {}, actual ) === true ) { + expectedOutput = null; + ok = true; + } + + QUnit.push( ok, actual, expectedOutput, message ); + } else { + QUnit.pushFailure( message, null, 'No exception was thrown.' ); + } + } +}; + +/** + * @deprecate since 1.8.0 + * Kept assertion helpers in root for backwards compatibility. + */ +extend( QUnit, assert ); + +/** + * @deprecated since 1.9.0 + * Kept root "raises()" for backwards compatibility. + * (Note that we don't introduce assert.raises). + */ +QUnit.raises = assert[ "throws" ]; + +/** + * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 + * Kept to avoid TypeErrors for undefined methods. + */ +QUnit.equals = function() { + QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); +}; +QUnit.same = function() { + QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); +}; + // We want access to the constructor's prototype (function() { function F() {} @@ -482,17 +680,11 @@ QUnit = { QUnit.constructor = F; }()); -// deprecated; still export them to window to provide clear error messages -// next step: remove entirely -QUnit.equals = function() { - QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); -}; -QUnit.same = function() { - QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); -}; - -// Maintain internal state -// `config` initialized at top of scope +/** + * Config object: Maintain internal state + * Later exposed as QUnit.config + * `config` initialized at top of scope + */ config = { // The queue of tests to run queue: [], @@ -511,7 +703,26 @@ config = { // by default, modify document.title when suite is done altertitle: true, - urlConfig: [ "noglobals", "notrycatch" ], + // when enabled, all tests must call expect() + requireExpects: false, + + // add checkboxes that are persisted in the query-string + // when enabled, the id is set to `true` as a `QUnit.config` property + urlConfig: [ + { + id: "noglobals", + label: "Check for Globals", + tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." + }, + { + id: "notrycatch", + label: "No try-catch", + tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." + } + ], + + // Set of all modules. + modules: {}, // logging callback queues begin: [], @@ -523,7 +734,16 @@ config = { moduleDone: [] }; -// Load paramaters +// Export global variables, unless an 'exports' object exists, +// in that case we assume we're in CommonJS (dealt with on the bottom of the script) +if ( typeof exports === "undefined" ) { + extend( window, QUnit ); + + // Expose QUnit object + window.QUnit = QUnit; +} + +// Initialize more QUnit.config and QUnit.urlParams (function() { var i, location = window.location || { search: "", protocol: "file:" }, @@ -543,21 +763,24 @@ config = { } QUnit.urlParams = urlParams; + + // String search anywhere in moduleName+testName config.filter = urlParams.filter; + // Exact match of the module name + config.module = urlParams.module; + + config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; + // Figure out if we're running the tests from a server or not QUnit.isLocal = location.protocol === "file:"; }()); -// Expose the API as global variables, unless an 'exports' object exists, -// in that case we assume we're in CommonJS - export everything at the end -if ( typeof exports === "undefined" ) { - extend( window, QUnit ); - window.QUnit = QUnit; -} - -// define these after exposing globals to keep them in these QUnit namespace only +// Extend QUnit object, +// these after set here because they should not be exposed as global functions extend( QUnit, { + assert: assert, + config: config, // Initialize the configuration options @@ -572,7 +795,7 @@ extend( QUnit, { autorun: false, filter: "", queue: [], - semaphore: 0 + semaphore: 1 }); var tests, banner, result, @@ -580,7 +803,7 @@ extend( QUnit, { if ( qunit ) { qunit.innerHTML = - "

" + escapeInnerText( document.title ) + "

" + + "

" + escapeText( document.title ) + "

" + "

" + "
" + "

" + @@ -613,17 +836,10 @@ extend( QUnit, { }, // Resets the test setup. Useful for tests that modify the DOM. - // If jQuery is available, uses jQuery's html(), otherwise just innerHTML. reset: function() { - var fixture; - - if ( window.jQuery ) { - jQuery( "#qunit-fixture" ).html( config.fixture ); - } else { - fixture = id( "qunit-fixture" ); - if ( fixture ) { - fixture.innerHTML = config.fixture; - } + var fixture = id( "qunit-fixture" ); + if ( fixture ) { + fixture.innerHTML = config.fixture; } }, @@ -643,7 +859,7 @@ extend( QUnit, { // Safe object type checking is: function( type, obj ) { - return QUnit.objectType( obj ) == type; + return QUnit.objectType( obj ) === type; }, objectType: function( obj ) { @@ -655,7 +871,8 @@ extend( QUnit, { return "null"; } - var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ""; + var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), + type = match && match[1] || ""; switch ( type ) { case "Number": @@ -684,22 +901,24 @@ extend( QUnit, { var output, source, details = { + module: config.current.module, + name: config.current.testName, result: result, message: message, actual: actual, expected: expected }; - message = escapeInnerText( message ) || ( result ? "okay" : "failed" ); + message = escapeText( message ) || ( result ? "okay" : "failed" ); message = "" + message + ""; output = message; if ( !result ) { - expected = escapeInnerText( QUnit.jsDump.parse(expected) ); - actual = escapeInnerText( QUnit.jsDump.parse(actual) ); + expected = escapeText( QUnit.jsDump.parse(expected) ); + actual = escapeText( QUnit.jsDump.parse(actual) ); output += ""; - if ( actual != expected ) { + if ( actual !== expected ) { output += ""; output += ""; } @@ -708,7 +927,7 @@ extend( QUnit, { if ( source ) { details.source = source; - output += ""; + output += ""; } output += "
Expected:
" + expected + "
Result:
" + actual + "
Diff:
" + QUnit.diff( expected, actual ) + "
Source:
" + escapeInnerText( source ) + "
Source:
" + escapeText( source ) + "
"; @@ -722,22 +941,36 @@ extend( QUnit, { }); }, - pushFailure: function( message, source ) { + pushFailure: function( message, source, actual ) { + if ( !config.current ) { + throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); + } + var output, details = { + module: config.current.module, + name: config.current.testName, result: false, message: message }; - message = escapeInnerText(message ) || "error"; + message = escapeText( message ) || "error"; message = "" + message + ""; output = message; + output += ""; + + if ( actual ) { + output += ""; + } + if ( source ) { details.source = source; - output += "
Result:
" + escapeText( actual ) + "
Source:
" + escapeInnerText( source ) + "
"; + output += "Source:
" + escapeText( source ) + "
"; } + output += ""; + runLoggingCallbacks( "log", QUnit, details ); config.current.assertions.push({ @@ -758,31 +991,44 @@ extend( QUnit, { querystring += encodeURIComponent( key ) + "=" + encodeURIComponent( params[ key ] ) + "&"; } - return window.location.pathname + querystring.slice( 0, -1 ); + return window.location.protocol + "//" + window.location.host + + window.location.pathname + querystring.slice( 0, -1 ); }, extend: extend, id: id, addEvent: addEvent + // load, equiv, jsDump, diff: Attached later }); -// QUnit.constructor is set to the empty F() above so that we can add to it's prototype later -// Doing this allows us to tell if the following methods have been overwritten on the actual -// QUnit object, which is a deprecated way of using the callbacks. +/** + * @deprecated: Created for backwards compatibility with test runner that set the hook function + * into QUnit.{hook}, instead of invoking it and passing the hook function. + * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. + * Doing this allows us to tell if the following methods have been overwritten on the actual + * QUnit object. + */ extend( QUnit.constructor.prototype, { + // Logging callbacks; all receive a single argument with the listed properties // run test/logs.html for any related changes begin: registerLoggingCallback( "begin" ), + // done: { failed, passed, total, runtime } done: registerLoggingCallback( "done" ), + // log: { result, actual, expected, message } log: registerLoggingCallback( "log" ), + // testStart: { name } testStart: registerLoggingCallback( "testStart" ), - // testDone: { name, failed, passed, total } + + // testDone: { name, failed, passed, total, duration } testDone: registerLoggingCallback( "testDone" ), + // moduleStart: { name } moduleStart: registerLoggingCallback( "moduleStart" ), + // moduleDone: { name, failed, passed, total } moduleDone: registerLoggingCallback( "moduleDone" ) }); @@ -796,6 +1042,9 @@ QUnit.load = function() { // Initialize the config, saving the execution queue var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, + urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, + numModules = 0, + moduleFilterHtml = "", urlConfigHtml = "", oldconfig = extend( {}, config ); @@ -808,10 +1057,36 @@ QUnit.load = function() { for ( i = 0; i < len; i++ ) { val = config.urlConfig[i]; - config[val] = QUnit.urlParams[val]; - urlConfigHtml += ""; + if ( typeof val === "string" ) { + val = { + id: val, + label: val, + tooltip: "[no tooltip available]" + }; + } + config[ val.id ] = QUnit.urlParams[ val.id ]; + urlConfigHtml += ""; } + moduleFilterHtml += ""; + // `userAgent` initialized at top of scope userAgent = id( "qunit-userAgent" ); if ( userAgent ) { @@ -821,12 +1096,7 @@ QUnit.load = function() { // `banner` initialized at top of scope banner = id( "qunit-header" ); if ( banner ) { - banner.innerHTML = "" + banner.innerHTML + " " + urlConfigHtml; - addEvent( banner, "change", function( event ) { - var params = {}; - params[ event.target.name ] = event.target.checked ? true : undefined; - window.location = QUnit.url( params ); - }); + banner.innerHTML = "" + banner.innerHTML + " "; } // `toolbar` initialized at top of scope @@ -867,8 +1137,37 @@ QUnit.load = function() { // `label` initialized at top of scope label = document.createElement( "label" ); label.setAttribute( "for", "qunit-filter-pass" ); + label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); label.innerHTML = "Hide passed tests"; toolbar.appendChild( label ); + + urlConfigCheckboxesContainer = document.createElement("span"); + urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; + urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); + // For oldIE support: + // * Add handlers to the individual elements instead of the container + // * Use "click" instead of "change" + // * Fallback from event.target to event.srcElement + addEvents( urlConfigCheckboxes, "click", function( event ) { + var params = {}, + target = event.target || event.srcElement; + params[ target.name ] = target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + toolbar.appendChild( urlConfigCheckboxesContainer ); + + if (numModules > 1) { + moduleFilter = document.createElement( 'span' ); + moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); + moduleFilter.innerHTML = moduleFilterHtml; + addEvent( moduleFilter.lastChild, "change", function() { + var selectBox = moduleFilter.getElementsByTagName("select")[0], + selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); + + window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); + }); + toolbar.appendChild(moduleFilter); + } } // `main` initialized at top of scope @@ -884,15 +1183,36 @@ QUnit.load = function() { addEvent( window, "load", QUnit.load ); -// addEvent(window, "error" ) gives us a useless event object -window.onerror = function( message, file, line ) { - if ( QUnit.config.current ) { - QUnit.pushFailure( message, file + ":" + line ); - } else { - QUnit.test( "global failure", function() { - QUnit.pushFailure( message, file + ":" + line ); - }); +// `onErrorFnPrev` initialized at top of scope +// Preserve other handlers +onErrorFnPrev = window.onerror; + +// Cover uncaught exceptions +// Returning true will surpress the default browser handler, +// returning false will let it run. +window.onerror = function ( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); } + + // Treat return value as window.onerror itself does, + // Only do our handling if not surpressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend( function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: validTest } ) ); + } + return false; + } + + return ret; }; function done() { @@ -919,7 +1239,7 @@ function done() { " milliseconds.
", "", passed, - " tests of ", + " assertions of ", config.stats.all, " passed, ", config.stats.bad, @@ -954,6 +1274,11 @@ function done() { } } + // scroll back to top to show results + if ( window.scrollTo ) { + window.scrollTo(0, 0); + } + runLoggingCallbacks( "done", QUnit, { failed: config.stats.bad, passed: passed, @@ -962,39 +1287,52 @@ function done() { }); } -function validTest( name ) { - var not, - filter = config.filter, - run = false; +/** @return Boolean: true if this test should be ran */ +function validTest( test ) { + var include, + filter = config.filter && config.filter.toLowerCase(), + module = config.module && config.module.toLowerCase(), + fullName = (test.module + ": " + test.testName).toLowerCase(); + + // Internally-generated tests are always valid + if ( test.callback && test.callback.validTest === validTest ) { + delete test.callback.validTest; + return true; + } + + if ( config.testNumber ) { + return test.testNumber === config.testNumber; + } + + if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { + return false; + } if ( !filter ) { return true; } - not = filter.charAt( 0 ) === "!"; - - if ( not ) { + include = filter.charAt( 0 ) !== "!"; + if ( !include ) { filter = filter.slice( 1 ); } - if ( name.indexOf( filter ) !== -1 ) { - return !not; + // If the filter matches, we need to honour include + if ( fullName.indexOf( filter ) !== -1 ) { + return include; } - if ( not ) { - run = true; - } - - return run; + // Otherwise, do the opposite + return !include; } // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) // Later Safari and IE10 are supposed to support error.stack as well // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack function extractStacktrace( e, offset ) { - offset = offset || 3; + offset = offset === undefined ? 3 : offset; - var stack; + var stack, include, i; if ( e.stacktrace ) { // Opera @@ -1005,6 +1343,18 @@ function extractStacktrace( e, offset ) { if (/^error$/i.test( stack[0] ) ) { stack.shift(); } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); + } + } return stack[ offset ]; } else if ( e.sourceURL ) { // Safari, PhantomJS @@ -1025,17 +1375,27 @@ function sourceFromStacktrace( offset ) { } } -function escapeInnerText( s ) { +/** + * Escape text for attribute or text content. + */ +function escapeText( s ) { if ( !s ) { return ""; } s = s + ""; - return s.replace( /[\&<>]/g, function( s ) { + // Both single quotes and double quotes (for attributes) + return s.replace( /['"<>&]/g, function( s ) { switch( s ) { - case "&": return "&"; - case "<": return "<"; - case ">": return ">"; - default: return s; + case '\'': + return '''; + case '"': + return '"'; + case '<': + return '<'; + case '>': + return '>'; + case '&': + return '&'; } }); } @@ -1083,7 +1443,7 @@ function saveGlobal() { } } -function checkPollution( name ) { +function checkPollution() { var newGlobals, deletedGlobals, old = config.pollution; @@ -1132,16 +1492,53 @@ function extend( a, b ) { return a; } +/** + * @param {HTMLElement} elem + * @param {string} type + * @param {Function} fn + */ function addEvent( elem, type, fn ) { + // Standards-based browsers if ( elem.addEventListener ) { elem.addEventListener( type, fn, false ); - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, fn ); + // IE } else { - fn(); + elem.attachEvent( "on" + type, fn ); } } +/** + * @param {Array|NodeList} elems + * @param {string} type + * @param {Function} fn + */ +function addEvents( elems, type, fn ) { + var i = elems.length; + while ( i-- ) { + addEvent( elems[i], type, fn ); + } +} + +function hasClass( elem, name ) { + return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; +} + +function addClass( elem, name ) { + if ( !hasClass( elem, name ) ) { + elem.className += (elem.className ? " " : "") + name; + } +} + +function removeClass( elem, name ) { + var set = " " + elem.className + " "; + // Class name may appear multiple times + while ( set.indexOf(" " + name + " ") > -1 ) { + set = set.replace(" " + name + " " , " "); + } + // If possible, trim it for prettiness, but not neccecarily + elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); +} + function id( name ) { return !!( typeof document !== "undefined" && document && document.getElementById ) && document.getElementById( name ); @@ -1155,7 +1552,6 @@ function registerLoggingCallback( key ) { // Supports deprecated method of completely overwriting logging callbacks function runLoggingCallbacks( key, scope, args ) { - //debugger; var i, callbacks; if ( QUnit.hasOwnProperty( key ) ) { QUnit[ key ].call(scope, args ); @@ -1197,6 +1593,7 @@ QUnit.equiv = (function() { // for string, boolean, number and null function useStrictEquality( b, a ) { + /*jshint eqeqeq:false */ if ( b instanceof a.constructor || a instanceof b.constructor ) { // to catch short annotaion VS 'new' annotation of a // declaration @@ -1231,7 +1628,8 @@ QUnit.equiv = (function() { a.global === b.global && // (gmi) ... a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; + a.multiline === b.multiline && + a.sticky === b.sticky; }, // - skip when the property is a method of an instance (OOP) @@ -1392,7 +1790,8 @@ QUnit.jsDump = (function() { var reName = /^function (\w+)/, jsDump = { - parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance + // type is used mostly internally, you can fix a (custom)type in advance + parse: function( obj, type, stack ) { stack = stack || [ ]; var inStack, res, parser = this.parsers[ type || this.typeOf(obj) ]; @@ -1400,18 +1799,16 @@ QUnit.jsDump = (function() { type = typeof parser; inStack = inArray( obj, stack ); - if ( inStack != -1 ) { + if ( inStack !== -1 ) { return "recursion(" + (inStack - stack.length) + ")"; } - //else - if ( type == "function" ) { + if ( type === "function" ) { stack.push( obj ); res = parser.call( this, obj, stack ); stack.pop(); return res; } - // else - return ( type == "string" ) ? parser : this.parsers.error; + return ( type === "string" ) ? parser : this.parsers.error; }, typeOf: function( obj ) { var type; @@ -1419,11 +1816,11 @@ QUnit.jsDump = (function() { type = "null"; } else if ( typeof obj === "undefined" ) { type = "undefined"; - } else if ( QUnit.is( "RegExp", obj) ) { + } else if ( QUnit.is( "regexp", obj) ) { type = "regexp"; - } else if ( QUnit.is( "Date", obj) ) { + } else if ( QUnit.is( "date", obj) ) { type = "date"; - } else if ( QUnit.is( "Function", obj) ) { + } else if ( QUnit.is( "function", obj) ) { type = "function"; } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { type = "window"; @@ -1438,6 +1835,8 @@ QUnit.jsDump = (function() { ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) ) { type = "array"; + } else if ( obj.constructor === Error.prototype.constructor ) { + type = "error"; } else { type = typeof obj; } @@ -1446,7 +1845,8 @@ QUnit.jsDump = (function() { separator: function() { return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; }, - indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + // extra can be a number, shortcut for increasing-calling-decreasing + indent: function( extra ) { if ( !this.multiline ) { return ""; } @@ -1475,13 +1875,16 @@ QUnit.jsDump = (function() { parsers: { window: "[Window]", document: "[Document]", - error: "[ERROR]", //when no parser is found, shouldn"t happen + error: function(error) { + return "Error(\"" + error.message + "\")"; + }, unknown: "[Unknown]", "null": "null", "undefined": "undefined", "function": function( fn ) { var ret = "function", - name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE + // functions never have name in IE + name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; if ( name ) { ret += " " + name; @@ -1497,13 +1900,9 @@ QUnit.jsDump = (function() { object: function( map, stack ) { var ret = [ ], keys, key, val, i; QUnit.jsDump.up(); - if ( Object.keys ) { - keys = Object.keys( map ); - } else { - keys = []; - for ( key in map ) { - keys.push( key ); - } + keys = []; + for ( key in map ) { + keys.push( key ); } keys.sort(); for ( i = 0; i < keys.length; i++ ) { @@ -1515,21 +1914,34 @@ QUnit.jsDump = (function() { return join( "{", ret, "}" ); }, node: function( node ) { - var a, val, + var len, i, val, open = QUnit.jsDump.HTML ? "<" : "<", close = QUnit.jsDump.HTML ? ">" : ">", tag = node.nodeName.toLowerCase(), - ret = open + tag; + ret = open + tag, + attrs = node.attributes; - for ( a in QUnit.jsDump.DOMAttrs ) { - val = node[ QUnit.jsDump.DOMAttrs[a] ]; - if ( val ) { - ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" ); + if ( attrs ) { + for ( i = 0, len = attrs.length; i < len; i++ ) { + val = attrs[i].nodeValue; + // IE6 includes all attributes in .attributes, even ones not explicitly set. + // Those have values like undefined, null, 0, false, "" or "inherit". + if ( val && val !== "inherit" ) { + ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); + } } } - return ret + close + open + "/" + tag + close; + ret += close; + + // Show content of TextNode or CDATASection + if ( node.nodeType === 3 || node.nodeType === 4 ) { + ret += node.nodeValue; + } + + return ret + open + "/" + tag + close; }, - functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function + // function calls it internally, it's the arguments part of the function + functionArgs: function( fn ) { var args, l = fn.length; @@ -1539,54 +1951,34 @@ QUnit.jsDump = (function() { args = new Array(l); while ( l-- ) { - args[l] = String.fromCharCode(97+l);//97 is 'a' + // 97 is 'a' + args[l] = String.fromCharCode(97+l); } return " " + args.join( ", " ) + " "; }, - key: quote, //object calls it internally, the key part of an item in a map - functionCode: "[code]", //function calls it internally, it's the content of the function - attribute: quote, //node calls it internally, it's an html attribute value + // object calls it internally, the key part of an item in a map + key: quote, + // function calls it internally, it's the content of the function + functionCode: "[code]", + // node calls it internally, it's an html attribute value + attribute: quote, string: quote, date: quote, - regexp: literal, //regex + regexp: literal, number: literal, "boolean": literal }, - DOMAttrs: { - //attributes to dump from nodes, name=>realName - id: "id", - name: "name", - "class": "className" - }, - HTML: false,//if true, entities are escaped ( <, >, \t, space and \n ) - indentChar: " ",//indentation unit - multiline: true //if true, items in a collection, are separated by a \n, else just a space. + // if true, entities are escaped ( <, >, \t, space and \n ) + HTML: false, + // indentation unit + indentChar: " ", + // if true, items in a collection, are separated by a \n, else just a space. + multiline: true }; return jsDump; }()); -// from Sizzle.js -function getText( elems ) { - var i, elem, - ret = ""; - - for ( i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += getText( elem.childNodes ); - } - } - - return ret; -} - // from jquery.js function inArray( elem, array ) { if ( array.indexOf ) { @@ -1617,13 +2009,14 @@ function inArray( elem, array ) { * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" */ QUnit.diff = (function() { + /*jshint eqeqeq:false, eqnull:true */ function diff( o, n ) { var i, ns = {}, os = {}; for ( i = 0; i < n.length; i++ ) { - if ( ns[ n[i] ] == null ) { + if ( !hasOwn.call( ns, n[i] ) ) { ns[ n[i] ] = { rows: [], o: null @@ -1633,7 +2026,7 @@ QUnit.diff = (function() { } for ( i = 0; i < o.length; i++ ) { - if ( os[ o[i] ] == null ) { + if ( !hasOwn.call( os, o[i] ) ) { os[ o[i] ] = { rows: [], n: null @@ -1646,7 +2039,7 @@ QUnit.diff = (function() { if ( !hasOwn.call( ns, i ) ) { continue; } - if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) { + if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] @@ -1752,7 +2145,7 @@ QUnit.diff = (function() { // for CommonJS enviroments, export everything if ( typeof exports !== "undefined" ) { - extend(exports, QUnit); + extend( exports, QUnit ); } // get at whatever the global object is, like window in browsers diff --git a/test/sinon/1.1.1/sinon.js b/test/sinon/1.1.1/sinon.js deleted file mode 100644 index 2de58701..00000000 --- a/test/sinon/1.1.1/sinon.js +++ /dev/null @@ -1,2820 +0,0 @@ -/** - * Sinon.JS 1.1.1, 2011/05/17 - * - * @author Christian Johansen (christian@cjohansen.no) - * - * (The BSD License) - * - * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * * Neither the name of Christian Johansen nor the names of his contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -"use strict"; -/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ -/*global module, require, __dirname, document*/ -/** - * Sinon core utilities. For internal use only. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -var sinon = (function () { - var div = typeof document != "undefined" && document.createElement("div"); - - function isNode(obj) { - var success = false; - - try { - obj.appendChild(div); - success = div.parentNode == obj; - } catch (e) { - return false; - } finally { - try { - obj.removeChild(div); - } catch (e) {} - } - - return success; - } - - function isElement(obj) { - return div && obj && obj.nodeType === 1 && isNode(obj); - } - - return { - wrapMethod: function wrapMethod(object, property, method) { - if (!object) { - throw new TypeError("Should wrap property of object"); - } - - if (typeof method != "function") { - throw new TypeError("Method wrapper should be function"); - } - - var wrappedMethod = object[property]; - var type = typeof wrappedMethod; - - if (type != "function") { - throw new TypeError("Attempted to wrap " + type + " property " + property + - " as function"); - } - - if (wrappedMethod.restore && wrappedMethod.restore.sinon) { - throw new TypeError("Attempted to wrap " + property + " which is already wrapped"); - } - - if (wrappedMethod.calledBefore) { - var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; - throw new TypeError("Attempted to wrap " + property + " which is already " + verb); - } - - object[property] = method; - method.displayName = property; - - method.restore = function () { - object[property] = wrappedMethod; - }; - - method.restore.sinon = true; - - return method; - }, - - extend: function extend(target) { - for (var i = 1, l = arguments.length; i < l; i += 1) { - for (var prop in arguments[i]) { - if (arguments[i].hasOwnProperty(prop)) { - target[prop] = arguments[i][prop]; - } - - // DONT ENUM bug, only care about toString - if (arguments[i].hasOwnProperty("toString") && - arguments[i].toString != target.toString) { - target.toString = arguments[i].toString; - } - } - } - - return target; - }, - - create: function create(proto) { - var F = function () {}; - F.prototype = proto; - return new F(); - }, - - deepEqual: function deepEqual(a, b) { - if (typeof a != "object" || typeof b != "object") { - return a === b; - } - - if (isElement(a) || isElement(b)) { - return a === b; - } - - if (a === b) { - return true; - } - - if (Object.prototype.toString.call(a) == "[object Array]") { - if (a.length !== b.length) { - return false; - } - - for (var i = 0, l = a.length; i < l; i += 1) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - - return true; - } - - var prop, aLength = 0, bLength = 0; - - for (prop in a) { - aLength += 1; - - if (!deepEqual(a[prop], b[prop])) { - return false; - } - } - - for (prop in b) { - bLength += 1; - } - - if (aLength != bLength) { - return false; - } - - return true; - }, - - keys: function keys(object) { - var objectKeys = []; - - for (var prop in object) { - if (object.hasOwnProperty(prop)) { - objectKeys.push(prop); - } - } - - return objectKeys.sort(); - }, - - functionName: function functionName(func) { - var name = func.displayName || func.name; - - // Use function decomposition as a last resort to get function - // name. Does not rely on function decomposition to work - if it - // doesn't debugging will be slightly less informative - // (i.e. toString will say 'spy' rather than 'myFunc'). - if (!name) { - var matches = func.toString().match(/function ([^\s\(]+)/); - name = matches && matches[1]; - } - - return name; - }, - - functionToString: function toString() { - if (this.getCall && this.callCount) { - var thisValue, prop, i = this.callCount; - - while (i--) { - thisValue = this.getCall(i).thisValue; - - for (prop in thisValue) { - if (thisValue[prop] === this) { - return prop; - } - } - } - } - - return this.displayName || "sinon fake"; - }, - - getConfig: function (custom) { - var config = {}; - custom = custom || {}; - var defaults = sinon.defaultConfig; - - for (var prop in defaults) { - if (defaults.hasOwnProperty(prop)) { - config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; - } - } - - return config; - }, - - format: function (val) { - return "" + val; - }, - - defaultConfig: { - injectIntoThis: true, - injectInto: null, - properties: ["spy", "stub", "mock", "clock", "server", "requests"], - useFakeTimers: true, - useFakeServer: true - } - }; -}()); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; - module.exports.spy = require("./sinon/spy"); - module.exports.stub = require("./sinon/stub"); - module.exports.mock = require("./sinon/mock"); - module.exports.collection = require("./sinon/collection"); - module.exports.assert = require("./sinon/assert"); - module.exports.sandbox = require("./sinon/sandbox"); - module.exports.test = require("./sinon/test"); - module.exports.testCase = require("./sinon/test_case"); - module.exports.assert = require("./sinon/assert"); -} - -/* @depend ../sinon.js */ -/*jslint eqeqeq: false, onevar: false, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Spy functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var spyCall; - var callId = 0; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function spy(object, property) { - if (!property && typeof object == "function") { - return spy.create(object); - } - - if (!object || !property) { - return spy.create(function () {}); - } - - var method = object[property]; - return sinon.wrapMethod(object, property, spy.create(method)); - } - - sinon.extend(spy, (function () { - var slice = Array.prototype.slice; - - function delegateToCalls(api, method, matchAny, actual) { - api[method] = function () { - if (!this.called) { - return false; - } - - var currentCall; - var matches = 0; - - for (var i = 0, l = this.callCount; i < l; i += 1) { - currentCall = this.getCall(i); - - if (currentCall[actual || method].apply(currentCall, arguments)) { - matches += 1; - - if (matchAny) { - return true; - } - } - } - - return matches === this.callCount; - }; - } - - function matchingFake(fakes, args, strict) { - if (!fakes) { - return; - } - - var alen = args.length; - - for (var i = 0, l = fakes.length; i < l; i++) { - if (fakes[i].matches(args, strict)) { - return fakes[i]; - } - } - } - - var uuid = 0; - - // Public API - var spyApi = { - reset: function () { - this.called = false; - this.calledOnce = false; - this.calledTwice = false; - this.calledThrice = false; - this.callCount = 0; - this.args = []; - this.returnValues = []; - this.thisValues = []; - this.exceptions = []; - this.callIds = []; - }, - - create: function create(func) { - var name; - - if (typeof func != "function") { - func = function () {}; - } else { - name = sinon.functionName(func); - } - - function proxy() { - return proxy.invoke(func, this, slice.call(arguments)); - } - - sinon.extend(proxy, spy); - delete proxy.create; - sinon.extend(proxy, func); - - proxy.reset(); - proxy.prototype = func.prototype; - proxy.displayName = name || "spy"; - proxy.toString = sinon.functionToString; - proxy._create = sinon.spy.create; - proxy.id = "spy#" + uuid++; - - return proxy; - }, - - invoke: function invoke(func, thisValue, args) { - var matching = matchingFake(this.fakes, args); - var exception, returnValue; - this.called = true; - this.callCount += 1; - this.calledOnce = this.callCount == 1; - this.calledTwice = this.callCount == 2; - this.calledThrice = this.callCount == 3; - this.thisValues.push(thisValue); - this.args.push(args); - this.callIds.push(callId++); - - try { - if (matching) { - returnValue = matching.invoke(func, thisValue, args); - } else { - returnValue = (this.func || func).apply(thisValue, args); - } - } catch (e) { - this.returnValues.push(undefined); - exception = e; - throw e; - } finally { - this.exceptions.push(exception); - } - - this.returnValues.push(returnValue); - - return returnValue; - }, - - getCall: function getCall(i) { - if (i < 0 || i >= this.callCount) { - return null; - } - - return spyCall.create(this, this.thisValues[i], this.args[i], - this.returnValues[i], this.exceptions[i], - this.callIds[i]); - }, - - calledBefore: function calledBefore(spyFn) { - if (!this.called) { - return false; - } - - if (!spyFn.called) { - return true; - } - - return this.callIds[0] < spyFn.callIds[0]; - }, - - calledAfter: function calledAfter(spyFn) { - if (!this.called || !spyFn.called) { - return false; - } - - return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; - }, - - withArgs: function () { - var args = slice.call(arguments); - - if (this.fakes) { - var match = matchingFake(this.fakes, args, true); - - if (match) { - return match; - } - } else { - this.fakes = []; - } - - var original = this; - var fake = this._create(); - fake.matchingAguments = args; - this.fakes.push(fake); - - fake.withArgs = function () { - return original.withArgs.apply(original, arguments); - }; - - return fake; - }, - - matches: function (args, strict) { - var margs = this.matchingAguments; - - if (margs.length <= args.length && - sinon.deepEqual(margs, args.slice(0, margs.length))) { - return !strict || margs.length == args.length; - } - } - }; - - delegateToCalls(spyApi, "calledOn", true); - delegateToCalls(spyApi, "alwaysCalledOn", false, "calledOn"); - delegateToCalls(spyApi, "calledWith", true); - delegateToCalls(spyApi, "alwaysCalledWith", false, "calledWith"); - delegateToCalls(spyApi, "calledWithExactly", true); - delegateToCalls(spyApi, "alwaysCalledWithExactly", false, "calledWithExactly"); - delegateToCalls(spyApi, "threw", true); - delegateToCalls(spyApi, "alwaysThrew", false, "threw"); - delegateToCalls(spyApi, "returned", true); - delegateToCalls(spyApi, "alwaysReturned", false, "returned"); - - return spyApi; - }())); - - spyCall = (function () { - return { - create: function create(spy, thisValue, args, returnValue, exception, id) { - var proxyCall = sinon.create(spyCall); - delete proxyCall.create; - proxyCall.proxy = spy; - proxyCall.thisValue = thisValue; - proxyCall.args = args; - proxyCall.returnValue = returnValue; - proxyCall.exception = exception; - proxyCall.callId = typeof id == "number" && id || callId++; - - return proxyCall; - }, - - calledOn: function calledOn(thisValue) { - return this.thisValue === thisValue; - }, - - calledWith: function calledWith() { - for (var i = 0, l = arguments.length; i < l; i += 1) { - if (!sinon.deepEqual(arguments[i], this.args[i])) { - return false; - } - } - - return true; - }, - - calledWithExactly: function calledWithExactly() { - return arguments.length == this.args.length && - this.calledWith.apply(this, arguments); - }, - - returned: function returned(value) { - return this.returnValue === value; - }, - - threw: function threw(error) { - if (typeof error == "undefined" || !this.exception) { - return !!this.exception; - } - - if (typeof error == "string") { - return this.exception.name == error; - } - - return this.exception === error; - }, - - calledBefore: function (other) { - return this.callId < other.callId; - }, - - calledAfter: function (other) { - return this.callId > other.callId; - }, - - toString: function () { - var callStr = this.proxy.toString() + "("; - var args = []; - - for (var i = 0, l = this.args.length; i < l; ++i) { - args.push(sinon.format(this.args[i])); - } - - callStr = callStr + args.join(", ") + ")"; - - if (typeof this.returnValue != "undefined") { - callStr += " => " + sinon.format(this.returnValue); - } - - if (this.exception) { - callStr += " !" + this.exception.name; - - if (this.exception.message) { - callStr += "(" + this.exception.message + ")"; - } - } - - return callStr; - } - }; - }()); - - if (commonJSModule) { - module.exports = spy; - } else { - sinon.spy = spy; - } - - sinon.spyCall = spyCall; -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend spy.js - */ -/*jslint eqeqeq: false, onevar: false*/ -/*global module, require, sinon*/ -/** - * Stub functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function stub(object, property, func) { - if (!!func && typeof func != "function") { - throw new TypeError("Custom stub should be function"); - } - - var wrapper; - - if (func) { - wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; - } else { - wrapper = stub.create(); - } - - if (!object && !property) { - return sinon.stub.create(); - } - - if (!property && !!object && typeof object == "object") { - for (var prop in object) { - if (object.hasOwnProperty(prop) && typeof object[prop] == "function") { - stub(object, prop); - } - } - - return object; - } - - return sinon.wrapMethod(object, property, wrapper); - } - - function getCallback(stub, args) { - if (stub.callArgAt < 0) { - for (var i = 0, l = args.length; i < l; ++i) { - if (!stub.callArgProp && typeof args[i] == "function") { - return args[i]; - } - - if (stub.callArgProp && args[i] && - typeof args[i][stub.callArgProp] == "function") { - return args[i][stub.callArgProp]; - } - } - - return null; - } - - return args[stub.callArgAt]; - } - - var join = Array.prototype.join; - - function getCallbackError(stub, func, args) { - if (stub.callArgAt < 0) { - var msg; - - if (stub.callArgProp) { - msg = sinon.functionName(stub) + - " expected to yield to '" + stub.callArgProp + - "', but no object with such a property was passed." - } else { - msg = sinon.functionName(stub) + - " expected to yield, but no callback was passed." - } - - if (args.length > 0) { - msg += " Received [" + join.call(args, ", ") + "]"; - } - - return msg; - } - - return "argument at index " + stub.callArgAt + " is not a function: " + func; - } - - function callCallback(stub, args) { - if (typeof stub.callArgAt == "number") { - var func = getCallback(stub, args); - - if (typeof func != "function") { - throw new TypeError(getCallbackError(stub, func, args)); - } - - func.apply(null, stub.callbackArguments); - } - } - - var uuid = 0; - - sinon.extend(stub, (function () { - var slice = Array.prototype.slice; - - function throwsException(error, message) { - if (typeof error == "string") { - this.exception = new Error(message || ""); - this.exception.name = error; - } else if (!error) { - this.exception = new Error("Error"); - } else { - this.exception = error; - } - - return this; - } - - return { - create: function create() { - var functionStub = function () { - if (functionStub.exception) { - throw functionStub.exception; - } - - callCallback(functionStub, arguments); - - return functionStub.returnValue; - }; - - functionStub.id = "stub#" + uuid++; - var orig = functionStub; - functionStub = sinon.spy.create(functionStub); - functionStub.func = orig; - - sinon.extend(functionStub, stub); - functionStub._create = sinon.stub.create; - functionStub.displayName = "stub"; - functionStub.toString = sinon.functionToString; - - return functionStub; - }, - - returns: function returns(value) { - this.returnValue = value; - - return this; - }, - - "throws": throwsException, - throwsException: throwsException, - - callsArg: function callsArg(pos) { - if (typeof pos != "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = []; - - return this; - }, - - callsArgWith: function callsArgWith(pos) { - if (typeof pos != "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 1); - - return this; - }, - - yields: function () { - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 0); - - return this; - }, - - yieldsTo: function (prop) { - this.callArgAt = -1; - this.callArgProp = prop; - this.callbackArguments = slice.call(arguments, 1); - - return this; - } - }; - }())); - - if (commonJSModule) { - module.exports = stub; - } else { - sinon.stub = stub; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - */ -/*jslint eqeqeq: false, onevar: false, nomen: false*/ -/*global module, require, sinon*/ -/** - * Mock functions. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function mock(object) { - if (!object) { - return sinon.expectation.create("Anonymous mock"); - } - - return mock.create(object); - } - - sinon.mock = mock; - - sinon.extend(mock, (function () { - function each(collection, callback) { - if (!collection) { - return; - } - - for (var i = 0, l = collection.length; i < l; i += 1) { - callback(collection[i]); - } - } - - return { - create: function create(object) { - if (!object) { - throw new TypeError("object is null"); - } - - var mockObject = sinon.extend({}, mock); - mockObject.object = object; - delete mockObject.create; - - return mockObject; - }, - - expects: function expects(method) { - if (!method) { - throw new TypeError("method is falsy"); - } - - if (!this.expectations) { - this.expectations = {}; - this.proxies = []; - } - - if (!this.expectations[method]) { - this.expectations[method] = []; - var mockObject = this; - - sinon.wrapMethod(this.object, method, function () { - return mockObject.invokeMethod(method, this, arguments); - }); - - this.proxies.push(method); - } - - var expectation = sinon.expectation.create(method); - this.expectations[method].push(expectation); - - return expectation; - }, - - restore: function restore() { - var object = this.object; - - each(this.proxies, function (proxy) { - if (typeof object[proxy].restore == "function") { - object[proxy].restore(); - } - }); - }, - - verify: function verify() { - var expectations = this.expectations || {}; - var messages = [], met = []; - - each(this.proxies, function (proxy) { - each(expectations[proxy], function (expectation) { - if (!expectation.met()) { - messages.push(expectation.toString()); - } else { - met.push(expectation.toString()); - } - }); - }); - - this.restore(); - - if (messages.length > 0) { - err(messages.concat(met).join("\n")); - } - - return true; - }, - - invokeMethod: function invokeMethod(method, thisValue, args) { - var expectations = this.expectations && this.expectations[method]; - var length = expectations && expectations.length || 0; - - for (var i = 0; i < length; i += 1) { - if (!expectations[i].met() && - expectations[i].allowsCall(thisValue, args)) { - return expectations[i].apply(thisValue, args); - } - } - - var messages = []; - - for (i = 0; i < length; i += 1) { - messages.push(" " + expectations[i].toString()); - } - - messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ - proxy: method, - args: args - })); - - err(messages.join("\n")); - } - }; - }())); - - function err(message) { - var exception = new Error(message); - exception.name = "ExpectationError"; - - throw exception; - } - - sinon.expectation = (function () { - var slice = Array.prototype.slice; - var _invoke = sinon.spy.invoke; - - function timesInWords(times) { - if (times == 0) { - return "never"; - } else if (times == 1) { - return "once"; - } else if (times == 2) { - return "twice"; - } else if (times == 3) { - return "thrice"; - } - - return times + " times"; - } - - function callCountInWords(callCount) { - if (callCount == 0) { - return "never called"; - } else { - return "called " + timesInWords(callCount); - } - } - - function expectedCallCountInWords(expectation) { - var min = expectation.minCalls; - var max = expectation.maxCalls; - - if (typeof min == "number" && typeof max == "number") { - var str = timesInWords(min); - - if (min != max) { - str = "at least " + str + " and at most " + timesInWords(max); - } - - return str; - } - - if (typeof min == "number") { - return "at least " + timesInWords(min); - } - - return "at most " + timesInWords(max); - } - - function receivedMinCalls(expectation) { - var hasMinLimit = typeof expectation.minCalls == "number"; - return !hasMinLimit || expectation.callCount >= expectation.minCalls; - } - - function receivedMaxCalls(expectation) { - if (typeof expectation.maxCalls != "number") { - return false; - } - - return expectation.callCount == expectation.maxCalls; - } - - return { - minCalls: 1, - maxCalls: 1, - - create: function create(methodName) { - var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); - delete expectation.create; - expectation.method = methodName; - - return expectation; - }, - - invoke: function invoke(func, thisValue, args) { - this.verifyCallAllowed(thisValue, args); - - return _invoke.apply(this, arguments); - }, - - atLeast: function atLeast(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.maxCalls = null; - this.limitsSet = true; - } - - this.minCalls = num; - - return this; - }, - - atMost: function atMost(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.minCalls = null; - this.limitsSet = true; - } - - this.maxCalls = num; - - return this; - }, - - never: function never() { - return this.exactly(0); - }, - - once: function once() { - return this.exactly(1); - }, - - twice: function twice() { - return this.exactly(2); - }, - - thrice: function thrice() { - return this.exactly(3); - }, - - exactly: function exactly(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not a number"); - } - - this.atLeast(num); - return this.atMost(num); - }, - - met: function met() { - return !this.failed && receivedMinCalls(this); - }, - - verifyCallAllowed: function verifyCallAllowed(thisValue, args) { - if (receivedMaxCalls(this)) { - this.failed = true; - err(this.method + " already called " + timesInWords(this.maxCalls)); - } - - if ("expectedThis" in this && this.expectedThis !== thisValue) { - err(this.method + " called with " + thisValue + " as thisValue, expected " + - this.expectedThis); - } - - if (!("expectedArguments" in this)) { - return; - } - - if (!args || args.length === 0) { - err(this.method + " received no arguments, expected " + - this.expectedArguments.join()); - } - - if (args.length < this.expectedArguments.length) { - err(this.method + " received too few arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - - if (this.expectsExactArgCount && - args.length != this.expectedArguments.length) { - err(this.method + " received too many arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - err(this.method + " received wrong arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - } - }, - - allowsCall: function allowsCall(thisValue, args) { - if (this.met()) { - return false; - } - - if ("expectedThis" in this && this.expectedThis !== thisValue) { - return false; - } - - if (!("expectedArguments" in this)) { - return true; - } - - args = args || []; - - if (args.length < this.expectedArguments.length) { - return false; - } - - if (this.expectsExactArgCount && - args.length != this.expectedArguments.length) { - return false; - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - return false; - } - } - - return true; - }, - - withArgs: function withArgs() { - this.expectedArguments = slice.call(arguments); - return this; - }, - - withExactArgs: function withExactArgs() { - this.withArgs.apply(this, arguments); - this.expectsExactArgCount = true; - return this; - }, - - on: function on(thisValue) { - this.expectedThis = thisValue; - return this; - }, - - toString: function () { - var args = (this.expectedArguments || []).slice(); - - if (!this.expectsExactArgCount) { - args.push("[...]"); - } - - var callStr = sinon.spyCall.toString.call({ - proxy: this.method, args: args - }); - - var message = callStr.replace(", [...", "[, ...") + " " + - expectedCallCountInWords(this); - - if (this.met()) { - return "Expectation met: " + message; - } - - return "Expected " + message + " (" + - callCountInWords(this.callCount) + ")"; - }, - - verify: function verify() { - if (!this.met()) { - err(this.toString()); - } - - return true; - } - }; - }()); - - if (commonJSModule) { - module.exports = mock; - } else { - sinon.mock = mock; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - */ -/*jslint eqeqeq: false, onevar: false, forin: true*/ -/*global module, require, sinon*/ -/** - * Collections of stubs, spies and mocks. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function getFakes(fakeCollection) { - if (!fakeCollection.fakes) { - fakeCollection.fakes = []; - } - - return fakeCollection.fakes; - } - - function each(fakeCollection, method) { - var fakes = getFakes(fakeCollection); - - for (var i = 0, l = fakes.length; i < l; i += 1) { - if (typeof fakes[i][method] == "function") { - fakes[i][method](); - } - } - } - - var collection = { - verify: function resolve() { - each(this, "verify"); - }, - - restore: function restore() { - each(this, "restore"); - }, - - verifyAndRestore: function verifyAndRestore() { - var exception; - - try { - this.verify(); - } catch (e) { - exception = e; - } - - this.restore(); - - if (exception) { - throw exception; - } - }, - - add: function add(fake) { - getFakes(this).push(fake); - - return fake; - }, - - spy: function spy() { - return this.add(sinon.spy.apply(sinon, arguments)); - }, - - stub: function stub(object, property, value) { - if (property) { - var original = object[property]; - - if (typeof original != "function") { - if (!object.hasOwnProperty(property)) { - throw new TypeError("Cannot stub non-existent own property " + property); - } - - object[property] = value; - - return this.add({ - restore: function () { - object[property] = original; - } - }); - } - } - - return this.add(sinon.stub.apply(sinon, arguments)); - }, - - mock: function mock() { - return this.add(sinon.mock.apply(sinon, arguments)); - }, - - inject: function inject(obj) { - var col = this; - - obj.spy = function () { - return col.spy.apply(col, arguments); - }; - - obj.stub = function () { - return col.stub.apply(col, arguments); - }; - - obj.mock = function () { - return col.mock.apply(col, arguments); - }; - - return obj; - } - }; - - if (commonJSModule) { - module.exports = collection; - } else { - sinon.collection = collection; - } -}(typeof sinon == "object" && sinon || null)); - -/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ -/*global module, require, window*/ -/** - * Fake timer API - * setTimeout - * setInterval - * clearTimeout - * clearInterval - * tick - * reset - * Date - * - * Inspired by jsUnitMockTimeOut from JsUnit - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - var sinon = {}; -} - -sinon.clock = (function () { - var id = 0; - - function addTimer(args, recurring) { - if (args.length === 0) { - throw new Error("Function requires at least 1 parameter"); - } - - var toId = id++; - var delay = args[1] || 0; - - if (!this.timeouts) { - this.timeouts = {}; - } - - this.timeouts[toId] = { - id: toId, - func: args[0], - callAt: this.now + delay - }; - - if (recurring === true) { - this.timeouts[toId].interval = delay; - } - - return toId; - } - - function parseTime(str) { - if (!str) { - return 0; - } - - var strings = str.split(":"); - var l = strings.length, i = l; - var ms = 0, parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error("tick only understands numbers and 'h:m:s'"); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - - if (parsed >= 60) { - throw new Error("Invalid time " + str); - } - - ms += parsed * Math.pow(60, (l - i - 1)); - } - - return ms * 1000; - } - - function createObject(object) { - var newObject; - - if (Object.create) { - newObject = Object.create(object); - } else { - var F = function () {}; - F.prototype = object; - newObject = new F(); - } - - newObject.Date.clock = newObject; - return newObject; - } - - return { - now: 0, - - create: function create(now) { - var clock = createObject(this); - - if (typeof now == "number") { - this.now = now; - } - - return clock; - }, - - setTimeout: function setTimeout(callback, timeout) { - return addTimer.call(this, arguments, false); - }, - - clearTimeout: function clearTimeout(timerId) { - if (!this.timeouts) { - this.timeouts = []; - } - - delete this.timeouts[timerId]; - }, - - setInterval: function setInterval(callback, timeout) { - return addTimer.call(this, arguments, true); - }, - - clearInterval: function clearInterval(timerId) { - this.clearTimeout(timerId); - }, - - tick: function tick(ms) { - ms = typeof ms == "number" ? ms : parseTime(ms); - var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; - var timer = this.firstTimerInRange(tickFrom, tickTo); - - while (timer && tickFrom <= tickTo) { - if (this.timeouts[timer.id]) { - tickFrom = this.now = timer.callAt; - this.callTimer(timer); - } - - timer = this.firstTimerInRange(previous, tickTo); - previous = tickFrom; - } - - this.now = tickTo; - }, - - firstTimerInRange: function (from, to) { - var timer, smallest, originalTimer; - - for (var id in this.timeouts) { - if (this.timeouts.hasOwnProperty(id)) { - if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { - continue; - } - - if (!smallest || this.timeouts[id].callAt < smallest) { - originalTimer = this.timeouts[id]; - smallest = this.timeouts[id].callAt; - - timer = { - func: this.timeouts[id].func, - callAt: this.timeouts[id].callAt, - interval: this.timeouts[id].interval, - id: this.timeouts[id].id - }; - } - } - } - - return timer || null; - }, - - callTimer: function (timer) { - try { - if (typeof timer.func == "function") { - timer.func.call(null); - } else { - eval(timer.func); - } - } catch (e) {} - - if (!this.timeouts[timer.id]) { - return; - } - - if (typeof timer.interval == "number") { - this.timeouts[timer.id].callAt += timer.interval; - } else { - delete this.timeouts[timer.id]; - } - }, - - reset: function reset() { - this.timeouts = {}; - }, - - Date: (function () { - var NativeDate = Date; - - function ClockDate(year, month, date, hour, minute, second, ms) { - // Defensive and verbose to avoid potential harm in passing - // explicit undefined when user does not pass argument - switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate(year, month, date, hour, minute, second); - default: - return new NativeDate(year, month, date, hour, minute, second, ms); - } - } - - if (NativeDate.now) { - ClockDate.now = function now() { - return ClockDate.clock.now; - }; - } - - if (NativeDate.toSource) { - ClockDate.toSource = function toSource() { - return NativeDate.toSource(); - }; - } - - ClockDate.toString = function toString() { - return NativeDate.toString(); - }; - - ClockDate.prototype = NativeDate.prototype; - ClockDate.parse = NativeDate.parse; - ClockDate.UTC = NativeDate.UTC; - - return ClockDate; - }()) - }; -}()); - -sinon.timers = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - Date: Date -}; - -sinon.useFakeTimers = (function (global) { - var methods = ["Date", "setTimeout", "setInterval", "clearTimeout", "clearInterval"]; - - function restore() { - var method; - - for (var i = 0, l = this.methods.length; i < l; i++) { - method = this.methods[i]; - global[method] = this["_" + method]; - } - } - - function stubGlobal(method, clock) { - clock["_" + method] = global[method]; - - global[method] = function () { - return clock[method].apply(clock, arguments); - }; - - for (var prop in clock[method]) { - if (clock[method].hasOwnProperty(prop)) { - global[method][prop] = clock[method][prop]; - } - } - - global[method].clock = clock; - } - - return function useFakeTimers(now) { - var clock = sinon.clock.create(now); - clock.restore = restore; - clock.methods = Array.prototype.slice.call(arguments, - typeof now == "number" ? 1 : 0); - - if (clock.methods.length === 0) { - clock.methods = methods; - } - - for (var i = 0, l = clock.methods.length; i < l; i++) { - stubGlobal(clock.methods[i], clock); - } - - return clock; - }; -}(typeof global != "undefined" ? global : this)); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/*jslint eqeqeq: false, onevar: false*/ -/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ -/** - * Fake XMLHttpRequest object - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - this.sinon = {}; -} - -sinon.xhr = { XMLHttpRequest: this.XMLHttpRequest }; - -sinon.FakeXMLHttpRequest = (function () { - /*jsl:ignore*/ - var unsafeHeaders = { - "Accept-Charset": true, - "Accept-Encoding": true, - "Connection": true, - "Content-Length": true, - "Cookie": true, - "Cookie2": true, - "Content-Transfer-Encoding": true, - "Date": true, - "Expect": true, - "Host": true, - "Keep-Alive": true, - "Referer": true, - "TE": true, - "Trailer": true, - "Transfer-Encoding": true, - "Upgrade": true, - "User-Agent": true, - "Via": true - }; - /*jsl:end*/ - - function FakeXMLHttpRequest() { - this.readyState = FakeXMLHttpRequest.UNSENT; - this.requestHeaders = {}; - this.requestBody = null; - this.status = 0; - this.statusText = ""; - - if (typeof FakeXMLHttpRequest.onCreate == "function") { - FakeXMLHttpRequest.onCreate(this); - } - } - - function verifyState(xhr) { - if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { - throw new Error("INVALID_STATE_ERR"); - } - - if (xhr.sendFlag) { - throw new Error("INVALID_STATE_ERR"); - } - } - - sinon.extend(FakeXMLHttpRequest.prototype, { - async: true, - - open: function open(method, url, async, username, password) { - this.method = method; - this.url = url; - this.async = typeof async == "boolean" ? async : true; - this.username = username; - this.password = password; - this.responseText = null; - this.responseXML = null; - this.requestHeaders = {}; - this.sendFlag = false; - this.readyStateChange(FakeXMLHttpRequest.OPENED); - }, - - readyStateChange: function readyStateChange(state) { - this.readyState = state; - - if (typeof this.onreadystatechange == "function") { - this.onreadystatechange(); - } - }, - - setRequestHeader: function setRequestHeader(header, value) { - verifyState(this); - - if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { - throw new Error("Refused to set unsafe header \"" + header + "\""); - } - - if (this.requestHeaders[header]) { - this.requestHeaders[header] += "," + value; - } else { - this.requestHeaders[header] = value; - } - }, - - // Helps testing - setResponseHeaders: function setResponseHeaders(headers) { - this.responseHeaders = {}; - - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - this.responseHeaders[header] = headers[header]; - } - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); - } - }, - - // Currently treats ALL data as a DOMString (i.e. no Document) - send: function send(data) { - verifyState(this); - - if (!/^(get|head)$/i.test(this.method)) { - if (this.requestHeaders["Content-Type"]) { - var value = this.requestHeaders["Content-Type"].split(";"); - this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; - } else { - this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; - } - - this.requestBody = data; - } - - this.errorFlag = false; - this.sendFlag = this.async; - this.readyStateChange(FakeXMLHttpRequest.OPENED); - - if (typeof this.onSend == "function") { - this.onSend(this); - } - }, - - abort: function abort() { - this.aborted = true; - this.responseText = null; - this.errorFlag = true; - this.requestHeaders = {}; - - if (this.readyState > sinon.FakeXMLHttpRequest.OPENED) { - this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); - this.sendFlag = false; - } - - this.readyState = sinon.FakeXMLHttpRequest.UNSENT; - }, - - getResponseHeader: function getResponseHeader(header) { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return null; - } - - if (/^Set-Cookie2?$/i.test(header)) { - return null; - } - - header = header.toLowerCase(); - - for (var h in this.responseHeaders) { - if (h.toLowerCase() == header) { - return this.responseHeaders[h]; - } - } - - return null; - }, - - getAllResponseHeaders: function getAllResponseHeaders() { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return ""; - } - - var headers = ""; - - for (var header in this.responseHeaders) { - if (this.responseHeaders.hasOwnProperty(header) && - !/^Set-Cookie2?$/i.test(header)) { - headers += header + ": " + this.responseHeaders[header] + "\r\n"; - } - } - - return headers; - }, - - setResponseBody: function setResponseBody(body) { - if (this.readyState == FakeXMLHttpRequest.DONE) { - throw new Error("Request done"); - } - - if (this.async && this.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { - throw new Error("No headers received"); - } - - var chunkSize = this.chunkSize || 10; - var index = 0; - this.responseText = ""; - - do { - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.LOADING); - } - - this.responseText += body.substring(index, index + chunkSize); - index += chunkSize; - } while (index < body.length); - - var type = this.getResponseHeader("Content-Type"); - - if (this.responseText && - (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { - try { - this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); - } catch (e) {} - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.DONE); - } else { - this.readyState = FakeXMLHttpRequest.DONE; - } - }, - - respond: function respond(status, headers, body) { - this.setResponseHeaders(headers || {}); - this.status = typeof status == "number" ? status : 200; - this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; - this.setResponseBody(body || ""); - } - }); - - sinon.extend(FakeXMLHttpRequest, { - UNSENT: 0, - OPENED: 1, - HEADERS_RECEIVED: 2, - LOADING: 3, - DONE: 4 - }); - - // Borrowed from JSpec - FakeXMLHttpRequest.parseXML = function parseXML(text) { - var xmlDoc; - - if (typeof DOMParser != "undefined") { - var parser = new DOMParser(); - xmlDoc = parser.parseFromString(text, "text/xml"); - } else { - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(text); - } - - return xmlDoc; - }; - - FakeXMLHttpRequest.statusCodes = { - 100: "Continue", - 101: "Switching Protocols", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 300: "Multiple Choice", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 307: "Temporary Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Request Entity Too Large", - 414: "Request-URI Too Long", - 415: "Unsupported Media Type", - 416: "Requested Range Not Satisfiable", - 417: "Expectation Failed", - 422: "Unprocessable Entity", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported" - }; - - return FakeXMLHttpRequest; -}()); - -(function (global) { - var GlobalXMLHttpRequest = global.XMLHttpRequest; - var GlobalActiveXObject = global.ActiveXObject; - var supportsActiveX = typeof ActiveXObject != "undefined"; - var supportsXHR = typeof XMLHttpRequest != "undefined"; - - sinon.useFakeXMLHttpRequest = function () { - sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { - if (supportsXHR) { - global.XMLHttpRequest = GlobalXMLHttpRequest; - } - - if (supportsActiveX) { - global.ActiveXObject = GlobalActiveXObject; - } - - delete sinon.FakeXMLHttpRequest.restore; - - if (keepOnCreate !== true) { - delete sinon.FakeXMLHttpRequest.onCreate; - } - }; - - if (supportsXHR) { - global.XMLHttpRequest = sinon.FakeXMLHttpRequest; - } - - if (supportsActiveX) { - global.ActiveXObject = function ActiveXObject(objId) { - if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/.test(objId)) { - return new sinon.FakeXMLHttpRequest(); - } - - return new GlobalActiveXObject(objId); - }; - } - - return sinon.FakeXMLHttpRequest; - }; -}(this)); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/** - * @depend fake_xml_http_request.js - */ -/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ -/*global module, require, window*/ -/** - * The Sinon "server" mimics a web server that receives requests from - * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, - * both synchronously and asynchronously. To respond synchronuously, canned - * answers have to be provided upfront. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - var sinon = {}; -} - -sinon.fakeServer = (function () { - function F() {} - - function create(proto) { - F.prototype = proto; - return new F(); - } - - function responseArray(handler) { - var response = handler; - - if (Object.prototype.toString.call(handler) != "[object Array]") { - response = [200, {}, handler]; - } - - if (typeof response[2] != "string") { - throw new TypeError("Fake server response body should be string, but was " + - typeof response[2]); - } - - return response; - } - - var wloc = window.location; - var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); - - function matchOne(response, reqMethod, reqUrl) { - var rmeth = response.method; - var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); - var url = response.url; - var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); - - return matchMethod && matchUrl; - } - - function match(response, request) { - var requestMethod = this.getHTTPMethod(request); - var requestUrl = request.url; - - if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { - requestUrl = requestUrl.replace(rCurrLoc, ""); - } - - if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { - if (typeof response.response == "function") { - var args = [request].concat(requestUrl.match(response.url).slice(1)); - return response.response.apply(response, args); - } - - return true; - } - - return false; - } - - return { - create: function () { - var server = create(this); - this.xhr = sinon.useFakeXMLHttpRequest(); - server.requests = []; - - this.xhr.onCreate = function (xhrObj) { - server.addRequest(xhrObj); - }; - - return server; - }, - - addRequest: function addRequest(xhrObj) { - var server = this; - this.requests.push(xhrObj); - - xhrObj.onSend = function () { - server.handleRequest(this); - }; - - if (this.autoRespond && !this.responding) { - setTimeout(function () { - server.responding = false; - server.respond(); - }, this.autoRespondAfter || 10); - - this.responding = true; - } - }, - - getHTTPMethod: function getHTTPMethod(request) { - if (this.fakeHTTPMethods && /post/i.test(request.method)) { - var matches = request.requestBody.match(/_method=([^\b;]+)/); - return !!matches ? matches[1] : request.method; - } - - return request.method; - }, - - handleRequest: function handleRequest(xhr) { - if (xhr.async) { - if (!this.queue) { - this.queue = []; - } - - this.queue.push(xhr); - } else { - this.processRequest(xhr); - } - }, - - respondWith: function respondWith(method, url, body) { - if (arguments.length == 1) { - this.response = responseArray(method); - } else { - if (!this.responses) { - this.responses = []; - } - - if (arguments.length == 2) { - body = url; - url = method; - method = null; - } - - this.responses.push({ - method: method, - url: url, - response: typeof body == "function" ? body : responseArray(body) - }); - } - }, - - respond: function respond() { - var queue = this.queue || []; - var request; - - while(request = queue.shift()) { - this.processRequest(request); - } - }, - - processRequest: function processRequest(request) { - try { - if (request.aborted) { - return; - } - - var response = this.response || [404, {}, ""]; - - if (this.responses) { - for (var i = 0, l = this.responses.length; i < l; i++) { - if (match.call(this, this.responses[i], request)) { - response = this.responses[i].response; - break; - } - } - } - - if (request.readyState != 4) { - request.respond(response[0], response[1], response[2]); - } - } catch (e) {} - }, - - restore: function restore() { - return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); - } - }; -}()); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/** - * @depend fake_server.js - * @depend fake_timers.js - */ -/*jslint browser: true, eqeqeq: false, onevar: false*/ -/*global sinon*/ -/** - * Add-on for sinon.fakeServer that automatically handles a fake timer along with - * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery - * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, - * it polls the object for completion with setInterval. Dispite the direct - * motivation, there is nothing jQuery-specific in this file, so it can be used - * in any environment where the ajax implementation depends on setInterval or - * setTimeout. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function () { - function Server() {} - Server.prototype = sinon.fakeServer; - - sinon.fakeServerWithClock = new Server(); - - sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { - if (xhr.async) { - if (typeof setTimeout.clock == "object") { - this.clock = setTimeout.clock; - } else { - this.clock = sinon.useFakeTimers(); - this.resetClock = true; - } - - if (!this.longestTimeout) { - var clockSetTimeout = this.clock.setTimeout; - var clockSetInterval = this.clock.setInterval; - var server = this; - - this.clock.setTimeout = function (fn, timeout) { - server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); - - return clockSetTimeout.apply(this, arguments); - }; - - this.clock.setInterval = function (fn, timeout) { - server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); - - return clockSetInterval.apply(this, arguments); - }; - } - } - - return sinon.fakeServer.addRequest.call(this, xhr); - }; - - sinon.fakeServerWithClock.respond = function respond() { - var returnVal = sinon.fakeServer.respond.apply(this, arguments); - - if (this.clock) { - this.clock.tick(this.longestTimeout || 0); - this.longestTimeout = 0; - - if (this.resetClock) { - this.clock.restore(); - this.resetClock = false; - } - } - - return returnVal; - }; - - sinon.fakeServerWithClock.restore = function restore() { - if (this.clock) { - this.clock.restore(); - } - - return sinon.fakeServer.restore.apply(this, arguments); - }; -}()); - -/** - * @depend ../sinon.js - * @depend collection.js - * @depend util/fake_timers.js - * @depend util/fake_server_with_clock.js - */ -/*jslint eqeqeq: false, onevar: false, plusplus: false*/ -/*global require, module*/ -/** - * Manages fake collections as well as fake utilities such as Sinon's - * timers and fake XHR implementation in one convenient object. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof module == "object" && typeof require == "function") { - var sinon = require("sinon"); - sinon.extend(sinon, require("./util/fake_timers")); -} - -(function () { - function exposeValue(sandbox, config, key, value) { - if (!value) { - return; - } - - if (config.injectInto) { - config.injectInto[key] = value; - } else { - sandbox.args.push(value); - } - } - - function prepareSandboxFromConfig(config) { - var sandbox = sinon.create(sinon.sandbox); - - if (config.useFakeServer) { - if (typeof config.useFakeServer == "object") { - sandbox.serverPrototype = config.useFakeServer; - } - - sandbox.useFakeServer(); - } - - if (config.useFakeTimers) { - if (typeof config.useFakeTimers == "object") { - sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); - } else { - sandbox.useFakeTimers(); - } - } - - return sandbox; - } - - sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { - useFakeTimers: function useFakeTimers() { - this.clock = sinon.useFakeTimers.apply(sinon, arguments); - - return this.add(this.clock); - }, - - serverPrototype: sinon.fakeServer, - - useFakeServer: function useFakeServer() { - var proto = this.serverPrototype || sinon.fakeServer; - - if (!proto || !proto.create) { - return null; - } - - this.server = proto.create(); - return this.add(this.server); - }, - - inject: function (obj) { - sinon.collection.inject.call(this, obj); - - if (this.clock) { - obj.clock = this.clock; - } - - if (this.server) { - obj.server = this.server; - obj.requests = this.server.requests; - } - - return obj; - }, - - create: function (config) { - if (!config) { - return sinon.create(sinon.sandbox); - } - - var sandbox = prepareSandboxFromConfig(config); - sandbox.args = sandbox.args || []; - var prop, value, exposed = sandbox.inject({}); - - if (config.properties) { - for (var i = 0, l = config.properties.length; i < l; i++) { - prop = config.properties[i]; - value = exposed[prop] || prop == "sandbox" && sandbox; - exposeValue(sandbox, config, prop, value); - } - } else { - exposeValue(sandbox, config, "sandbox", value); - } - - return sandbox; - } - }); - - sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; - - if (typeof module != "undefined") { - module.exports = sinon.sandbox; - } -}()); - -/** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - * @depend sandbox.js - */ -/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Test function, sandboxes fakes - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function test(callback) { - var type = typeof callback; - - if (type != "function") { - throw new TypeError("sinon.test needs to wrap a test function, got " + type); - } - - return function () { - var config = sinon.getConfig(sinon.config); - config.injectInto = config.injectIntoThis && this || config.injectInto; - var sandbox = sinon.sandbox.create(config); - var exception, result; - var args = Array.prototype.slice.call(arguments).concat(sandbox.args); - - try { - result = callback.apply(this, args); - } catch (e) { - exception = e; - } - - sandbox.verifyAndRestore(); - - if (exception) { - throw exception; - } - - return result; - }; - } - - test.config = { - injectIntoThis: true, - injectInto: null, - properties: ["spy", "stub", "mock", "clock", "server", "requests"], - useFakeTimers: true, - useFakeServer: true - }; - - if (commonJSModule) { - module.exports = test; - } else { - sinon.test = test; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend test.js - */ -/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ -/*global module, require, sinon*/ -/** - * Test case, sandboxes all test functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon || !Object.prototype.hasOwnProperty) { - return; - } - - function createTest(property, setUp, tearDown) { - return function () { - if (setUp) { - setUp.apply(this, arguments); - } - - var exception, result; - - try { - result = property.apply(this, arguments); - } catch (e) { - exception = e; - } - - if (tearDown) { - tearDown.apply(this, arguments); - } - - if (exception) { - throw exception; - } - - return result; - }; - } - - function testCase(tests, prefix) { - /*jsl:ignore*/ - if (!tests || typeof tests != "object") { - throw new TypeError("sinon.testCase needs an object with test functions"); - } - /*jsl:end*/ - - prefix = prefix || "test"; - var rPrefix = new RegExp("^" + prefix); - var methods = {}, testName, property, method; - var setUp = tests.setUp; - var tearDown = tests.tearDown; - - for (testName in tests) { - if (tests.hasOwnProperty(testName)) { - property = tests[testName]; - - if (/^(setUp|tearDown)$/.test(testName)) { - continue; - } - - if (typeof property == "function" && rPrefix.test(testName)) { - method = property; - - if (setUp || tearDown) { - method = createTest(property, setUp, tearDown); - } - - methods[testName] = sinon.test(method); - } else { - methods[testName] = tests[testName]; - } - } - } - - return methods; - } - - if (commonJSModule) { - module.exports = testCase; - } else { - sinon.testCase = testCase; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - */ -/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Assertions matching the test spy retrieval interface. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var slice = Array.prototype.slice; - var assert; - - if (!sinon && commonJSModule) { - sinon = require("sinon"); - } - - if (!sinon) { - return; - } - - function times(count) { - return count == 1 && "once" || - count == 2 && "twice" || - count == 3 && "thrice" || - (count || 0) + " times"; - } - - function verifyIsStub(method) { - if (!method) { - assert.fail("fake is not a spy"); - } - - if (typeof method != "function") { - assert.fail(method + " is not a function"); - } - - if (typeof method.getCall != "function") { - assert.fail(method + " is not stubbed"); - } - } - - function failAssertion(object, msg) { - var failMethod = object.fail || assert.fail; - failMethod.call(object, msg); - } - - function mirrorAssertion(method, message) { - assert[method] = function (fake) { - verifyIsStub(fake); - - var failed = typeof fake[method] == "function" ? - !fake[method].apply(fake, slice.call(arguments, 1)) : !fake[method]; - - if (failed) { - var msg = message.replace("%c", times(fake.callCount)); - msg = msg.replace("%n", fake + ""); - - msg = msg.replace("%C", function (m) { - return formatSpyCalls(fake); - }); - - msg = msg.replace("%t", function (m) { - return formatThisValues(fake); - }); - - msg = msg.replace("%*", [].slice.call(arguments, 1).join(", ")); - - for (var i = 0, l = arguments.length; i < l; i++) { - msg = msg.replace("%" + i, arguments[i]); - } - - failAssertion(this, msg); - } else { - assert.pass(method); - } - }; - } - - function formatSpyCalls(spy) { - var calls = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - calls.push(" " + spy.getCall(i).toString()); - } - - return calls.length > 0 ? "\n" + calls.join("\n") : ""; - } - - function formatThisValues(spy) { - var objects = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - objects.push(sinon.format(spy.thisValues[i])); - } - - return objects.join(", "); - } - - assert = { - failException: "AssertError", - - fail: function fail(message) { - var error = new Error(message); - error.name = this.failException || assert.failException; - - throw error; - }, - - pass: function pass(assertion) {}, - - called: function assertCalled(method) { - verifyIsStub(method); - - if (!method.called) { - failAssertion(this, "expected " + method + - " to have been called at least once but was never called"); - } else { - assert.pass("called"); - } - }, - - notCalled: function assertNotCalled(method) { - verifyIsStub(method); - - if (method.called) { - failAssertion( - this, "expected " + method + " to not have been called but was " + - "called " + times(method.callCount) + formatSpyCalls(method)); - } else { - assert.pass("notCalled"); - } - }, - - callOrder: function assertCallOrder() { - verifyIsStub(arguments[0]); - var expected = []; - var actual = []; - var failed = false; - expected.push(arguments[0]); - - for (var i = 1, l = arguments.length; i < l; i++) { - verifyIsStub(arguments[i]); - expected.push(arguments[i]); - - if (!arguments[i - 1].calledBefore(arguments[i])) { - failed = true; - } - } - - if (failed) { - actual = [].concat(expected).sort(function (a, b) { - var aId = a.getCall(0).callId; - var bId = b.getCall(0).callId; - - // uuid, won't ever be equal - return aId < bId ? -1 : 1; - }); - - var expectedStr, actualStr; - - try { - expectedStr = expected.join(", "); - actualStr = actual.join(", "); - } catch (e) {} - - failAssertion(this, "expected " + (expectedStr || "") + " to be " + - "called in order but were called as " + actualStr); - } else { - assert.pass("callOrder"); - } - }, - - callCount: function assertCallCount(method, count) { - verifyIsStub(method); - - if (method.callCount != count) { - failAssertion(this, "expected " + method + " to be called " + - times(count) + " but was called " + - times(method.callCount) + formatSpyCalls(method)); - } else { - assert.pass("callCount"); - } - }, - - expose: function expose(target, options) { - if (!target) { - throw new TypeError("target is null or undefined"); - } - - options = options || {}; - var prefix = typeof options.prefix == "undefined" && "assert" || options.prefix; - - var name = function (prop) { - if (!prefix) { - return prop; - } - - return prefix + prop.substring(0, 1).toUpperCase() + prop.substring(1); - }; - - for (var assertion in this) { - if (!/^(fail|expose)/.test(assertion)) { - target[name(assertion)] = this[assertion]; - } - } - - if (typeof options.includeFail == "undefined" || !!options.includeFail) { - target.fail = this.fail; - target.failException = this.failException; - } - - return target; - } - }; - - mirrorAssertion("calledOnce", "expected %n to be called once but was called %c%C"); - mirrorAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); - mirrorAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); - mirrorAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); - mirrorAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); - mirrorAssertion("calledWith", "expected %n to be called with arguments %*%C"); - mirrorAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); - mirrorAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); - mirrorAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); - mirrorAssertion("threw", "%n did not throw exception%C"); - mirrorAssertion("alwaysThrew", "%n did not always throw exception%C"); - - if (commonJSModule) { - module.exports = assert; - } else { - sinon.assert = assert; - } -}(typeof sinon == "object" && sinon || null)); diff --git a/test/sinon/1.7.1/sinon.js b/test/sinon/1.7.1/sinon.js new file mode 100644 index 00000000..589c0f57 --- /dev/null +++ b/test/sinon/1.7.1/sinon.js @@ -0,0 +1,4299 @@ +/** + * Sinon.JS 1.7.1, 2013/05/07 + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS + * + * (The BSD License) + * + * Copyright (c) 2010-2013, Christian Johansen, christian@cjohansen.no + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Christian Johansen nor the names of his contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +this.sinon = (function () { +var buster = (function (setTimeout, B) { + var isNode = typeof require == "function" && typeof module == "object"; + var div = typeof document != "undefined" && document.createElement("div"); + var F = function () {}; + + var buster = { + bind: function bind(obj, methOrProp) { + var method = typeof methOrProp == "string" ? obj[methOrProp] : methOrProp; + var args = Array.prototype.slice.call(arguments, 2); + return function () { + var allArgs = args.concat(Array.prototype.slice.call(arguments)); + return method.apply(obj, allArgs); + }; + }, + + partial: function partial(fn) { + var args = [].slice.call(arguments, 1); + return function () { + return fn.apply(this, args.concat([].slice.call(arguments))); + }; + }, + + create: function create(object) { + F.prototype = object; + return new F(); + }, + + extend: function extend(target) { + if (!target) { return; } + for (var i = 1, l = arguments.length, prop; i < l; ++i) { + for (prop in arguments[i]) { + target[prop] = arguments[i][prop]; + } + } + return target; + }, + + nextTick: function nextTick(callback) { + if (typeof process != "undefined" && process.nextTick) { + return process.nextTick(callback); + } + setTimeout(callback, 0); + }, + + functionName: function functionName(func) { + if (!func) return ""; + if (func.displayName) return func.displayName; + if (func.name) return func.name; + var matches = func.toString().match(/function\s+([^\(]+)/m); + return matches && matches[1] || ""; + }, + + isNode: function isNode(obj) { + if (!div) return false; + try { + obj.appendChild(div); + obj.removeChild(div); + } catch (e) { + return false; + } + return true; + }, + + isElement: function isElement(obj) { + return obj && obj.nodeType === 1 && buster.isNode(obj); + }, + + isArray: function isArray(arr) { + return Object.prototype.toString.call(arr) == "[object Array]"; + }, + + flatten: function flatten(arr) { + var result = [], arr = arr || []; + for (var i = 0, l = arr.length; i < l; ++i) { + result = result.concat(buster.isArray(arr[i]) ? flatten(arr[i]) : arr[i]); + } + return result; + }, + + each: function each(arr, callback) { + for (var i = 0, l = arr.length; i < l; ++i) { + callback(arr[i]); + } + }, + + map: function map(arr, callback) { + var results = []; + for (var i = 0, l = arr.length; i < l; ++i) { + results.push(callback(arr[i])); + } + return results; + }, + + parallel: function parallel(fns, callback) { + function cb(err, res) { + if (typeof callback == "function") { + callback(err, res); + callback = null; + } + } + if (fns.length == 0) { return cb(null, []); } + var remaining = fns.length, results = []; + function makeDone(num) { + return function done(err, result) { + if (err) { return cb(err); } + results[num] = result; + if (--remaining == 0) { cb(null, results); } + }; + } + for (var i = 0, l = fns.length; i < l; ++i) { + fns[i](makeDone(i)); + } + }, + + series: function series(fns, callback) { + function cb(err, res) { + if (typeof callback == "function") { + callback(err, res); + } + } + var remaining = fns.slice(); + var results = []; + function callNext() { + if (remaining.length == 0) return cb(null, results); + var promise = remaining.shift()(next); + if (promise && typeof promise.then == "function") { + promise.then(buster.partial(next, null), next); + } + } + function next(err, result) { + if (err) return cb(err); + results.push(result); + callNext(); + } + callNext(); + }, + + countdown: function countdown(num, done) { + return function () { + if (--num == 0) done(); + }; + } + }; + + if (typeof process === "object" && + typeof require === "function" && typeof module === "object") { + var crypto = require("crypto"); + var path = require("path"); + + buster.tmpFile = function (fileName) { + var hashed = crypto.createHash("sha1"); + hashed.update(fileName); + var tmpfileName = hashed.digest("hex"); + + if (process.platform == "win32") { + return path.join(process.env["TEMP"], tmpfileName); + } else { + return path.join("/tmp", tmpfileName); + } + }; + } + + if (Array.prototype.some) { + buster.some = function (arr, fn, thisp) { + return arr.some(fn, thisp); + }; + } else { + // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some + buster.some = function (arr, fun, thisp) { + if (arr == null) { throw new TypeError(); } + arr = Object(arr); + var len = arr.length >>> 0; + if (typeof fun !== "function") { throw new TypeError(); } + + for (var i = 0; i < len; i++) { + if (arr.hasOwnProperty(i) && fun.call(thisp, arr[i], i, arr)) { + return true; + } + } + + return false; + }; + } + + if (Array.prototype.filter) { + buster.filter = function (arr, fn, thisp) { + return arr.filter(fn, thisp); + }; + } else { + // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter + buster.filter = function (fn, thisp) { + if (this == null) { throw new TypeError(); } + + var t = Object(this); + var len = t.length >>> 0; + if (typeof fn != "function") { throw new TypeError(); } + + var res = []; + for (var i = 0; i < len; i++) { + if (i in t) { + var val = t[i]; // in case fun mutates this + if (fn.call(thisp, val, i, t)) { res.push(val); } + } + } + + return res; + }; + } + + if (isNode) { + module.exports = buster; + buster.eventEmitter = require("./buster-event-emitter"); + Object.defineProperty(buster, "defineVersionGetter", { + get: function () { + return require("./define-version-getter"); + } + }); + } + + return buster.extend(B || {}, buster); +}(setTimeout, buster)); +if (typeof buster === "undefined") { + var buster = {}; +} + +if (typeof module === "object" && typeof require === "function") { + buster = require("buster-core"); +} + +buster.format = buster.format || {}; +buster.format.excludeConstructors = ["Object", /^.$/]; +buster.format.quoteStrings = true; + +buster.format.ascii = (function () { + + var hasOwn = Object.prototype.hasOwnProperty; + + var specialObjects = []; + if (typeof global != "undefined") { + specialObjects.push({ obj: global, value: "[object global]" }); + } + if (typeof document != "undefined") { + specialObjects.push({ obj: document, value: "[object HTMLDocument]" }); + } + if (typeof window != "undefined") { + specialObjects.push({ obj: window, value: "[object Window]" }); + } + + function keys(object) { + var k = Object.keys && Object.keys(object) || []; + + if (k.length == 0) { + for (var prop in object) { + if (hasOwn.call(object, prop)) { + k.push(prop); + } + } + } + + return k.sort(); + } + + function isCircular(object, objects) { + if (typeof object != "object") { + return false; + } + + for (var i = 0, l = objects.length; i < l; ++i) { + if (objects[i] === object) { + return true; + } + } + + return false; + } + + function ascii(object, processed, indent) { + if (typeof object == "string") { + var quote = typeof this.quoteStrings != "boolean" || this.quoteStrings; + return processed || quote ? '"' + object + '"' : object; + } + + if (typeof object == "function" && !(object instanceof RegExp)) { + return ascii.func(object); + } + + processed = processed || []; + + if (isCircular(object, processed)) { + return "[Circular]"; + } + + if (Object.prototype.toString.call(object) == "[object Array]") { + return ascii.array.call(this, object, processed); + } + + if (!object) { + return "" + object; + } + + if (buster.isElement(object)) { + return ascii.element(object); + } + + if (typeof object.toString == "function" && + object.toString !== Object.prototype.toString) { + return object.toString(); + } + + for (var i = 0, l = specialObjects.length; i < l; i++) { + if (object === specialObjects[i].obj) { + return specialObjects[i].value; + } + } + + return ascii.object.call(this, object, processed, indent); + } + + ascii.func = function (func) { + return "function " + buster.functionName(func) + "() {}"; + }; + + ascii.array = function (array, processed) { + processed = processed || []; + processed.push(array); + var pieces = []; + + for (var i = 0, l = array.length; i < l; ++i) { + pieces.push(ascii.call(this, array[i], processed)); + } + + return "[" + pieces.join(", ") + "]"; + }; + + ascii.object = function (object, processed, indent) { + processed = processed || []; + processed.push(object); + indent = indent || 0; + var pieces = [], properties = keys(object), prop, str, obj; + var is = ""; + var length = 3; + + for (var i = 0, l = indent; i < l; ++i) { + is += " "; + } + + for (i = 0, l = properties.length; i < l; ++i) { + prop = properties[i]; + obj = object[prop]; + + if (isCircular(obj, processed)) { + str = "[Circular]"; + } else { + str = ascii.call(this, obj, processed, indent + 2); + } + + str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str; + length += str.length; + pieces.push(str); + } + + var cons = ascii.constructorName.call(this, object); + var prefix = cons ? "[" + cons + "] " : "" + + return (length + indent) > 80 ? + prefix + "{\n " + is + pieces.join(",\n " + is) + "\n" + is + "}" : + prefix + "{ " + pieces.join(", ") + " }"; + }; + + ascii.element = function (element) { + var tagName = element.tagName.toLowerCase(); + var attrs = element.attributes, attribute, pairs = [], attrName; + + for (var i = 0, l = attrs.length; i < l; ++i) { + attribute = attrs.item(i); + attrName = attribute.nodeName.toLowerCase().replace("html:", ""); + + if (attrName == "contenteditable" && attribute.nodeValue == "inherit") { + continue; + } + + if (!!attribute.nodeValue) { + pairs.push(attrName + "=\"" + attribute.nodeValue + "\""); + } + } + + var formatted = "<" + tagName + (pairs.length > 0 ? " " : ""); + var content = element.innerHTML; + + if (content.length > 20) { + content = content.substr(0, 20) + "[...]"; + } + + var res = formatted + pairs.join(" ") + ">" + content + ""; + + return res.replace(/ contentEditable="inherit"/, ""); + }; + + ascii.constructorName = function (object) { + var name = buster.functionName(object && object.constructor); + var excludes = this.excludeConstructors || buster.format.excludeConstructors || []; + + for (var i = 0, l = excludes.length; i < l; ++i) { + if (typeof excludes[i] == "string" && excludes[i] == name) { + return ""; + } else if (excludes[i].test && excludes[i].test(name)) { + return ""; + } + } + + return name; + }; + + return ascii; +}()); + +if (typeof module != "undefined") { + module.exports = buster.format; +} +/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ +/*global module, require, __dirname, document*/ +/** + * Sinon core utilities. For internal use only. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +var sinon = (function (buster) { + var div = typeof document != "undefined" && document.createElement("div"); + var hasOwn = Object.prototype.hasOwnProperty; + + function isDOMNode(obj) { + var success = false; + + try { + obj.appendChild(div); + success = div.parentNode == obj; + } catch (e) { + return false; + } finally { + try { + obj.removeChild(div); + } catch (e) { + // Remove failed, not much we can do about that + } + } + + return success; + } + + function isElement(obj) { + return div && obj && obj.nodeType === 1 && isDOMNode(obj); + } + + function isFunction(obj) { + return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply); + } + + function mirrorProperties(target, source) { + for (var prop in source) { + if (!hasOwn.call(target, prop)) { + target[prop] = source[prop]; + } + } + } + + function isRestorable (obj) { + return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon; + } + + var sinon = { + wrapMethod: function wrapMethod(object, property, method) { + if (!object) { + throw new TypeError("Should wrap property of object"); + } + + if (typeof method != "function") { + throw new TypeError("Method wrapper should be function"); + } + + var wrappedMethod = object[property]; + + if (!isFunction(wrappedMethod)) { + throw new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " + + property + " as function"); + } + + if (wrappedMethod.restore && wrappedMethod.restore.sinon) { + throw new TypeError("Attempted to wrap " + property + " which is already wrapped"); + } + + if (wrappedMethod.calledBefore) { + var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; + throw new TypeError("Attempted to wrap " + property + " which is already " + verb); + } + + // IE 8 does not support hasOwnProperty on the window object. + var owned = hasOwn.call(object, property); + object[property] = method; + method.displayName = property; + + method.restore = function () { + // For prototype properties try to reset by delete first. + // If this fails (ex: localStorage on mobile safari) then force a reset + // via direct assignment. + if (!owned) { + delete object[property]; + } + if (object[property] === method) { + object[property] = wrappedMethod; + } + }; + + method.restore.sinon = true; + mirrorProperties(method, wrappedMethod); + + return method; + }, + + extend: function extend(target) { + for (var i = 1, l = arguments.length; i < l; i += 1) { + for (var prop in arguments[i]) { + if (arguments[i].hasOwnProperty(prop)) { + target[prop] = arguments[i][prop]; + } + + // DONT ENUM bug, only care about toString + if (arguments[i].hasOwnProperty("toString") && + arguments[i].toString != target.toString) { + target.toString = arguments[i].toString; + } + } + } + + return target; + }, + + create: function create(proto) { + var F = function () {}; + F.prototype = proto; + return new F(); + }, + + deepEqual: function deepEqual(a, b) { + if (sinon.match && sinon.match.isMatcher(a)) { + return a.test(b); + } + if (typeof a != "object" || typeof b != "object") { + return a === b; + } + + if (isElement(a) || isElement(b)) { + return a === b; + } + + if (a === b) { + return true; + } + + if ((a === null && b !== null) || (a !== null && b === null)) { + return false; + } + + var aString = Object.prototype.toString.call(a); + if (aString != Object.prototype.toString.call(b)) { + return false; + } + + if (aString == "[object Array]") { + if (a.length !== b.length) { + return false; + } + + for (var i = 0, l = a.length; i < l; i += 1) { + if (!deepEqual(a[i], b[i])) { + return false; + } + } + + return true; + } + + var prop, aLength = 0, bLength = 0; + + for (prop in a) { + aLength += 1; + + if (!deepEqual(a[prop], b[prop])) { + return false; + } + } + + for (prop in b) { + bLength += 1; + } + + return aLength == bLength; + }, + + functionName: function functionName(func) { + var name = func.displayName || func.name; + + // Use function decomposition as a last resort to get function + // name. Does not rely on function decomposition to work - if it + // doesn't debugging will be slightly less informative + // (i.e. toString will say 'spy' rather than 'myFunc'). + if (!name) { + var matches = func.toString().match(/function ([^\s\(]+)/); + name = matches && matches[1]; + } + + return name; + }, + + functionToString: function toString() { + if (this.getCall && this.callCount) { + var thisValue, prop, i = this.callCount; + + while (i--) { + thisValue = this.getCall(i).thisValue; + + for (prop in thisValue) { + if (thisValue[prop] === this) { + return prop; + } + } + } + } + + return this.displayName || "sinon fake"; + }, + + getConfig: function (custom) { + var config = {}; + custom = custom || {}; + var defaults = sinon.defaultConfig; + + for (var prop in defaults) { + if (defaults.hasOwnProperty(prop)) { + config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; + } + } + + return config; + }, + + format: function (val) { + return "" + val; + }, + + defaultConfig: { + injectIntoThis: true, + injectInto: null, + properties: ["spy", "stub", "mock", "clock", "server", "requests"], + useFakeTimers: true, + useFakeServer: true + }, + + timesInWords: function timesInWords(count) { + return count == 1 && "once" || + count == 2 && "twice" || + count == 3 && "thrice" || + (count || 0) + " times"; + }, + + calledInOrder: function (spies) { + for (var i = 1, l = spies.length; i < l; i++) { + if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) { + return false; + } + } + + return true; + }, + + orderByFirstCall: function (spies) { + return spies.sort(function (a, b) { + // uuid, won't ever be equal + var aCall = a.getCall(0); + var bCall = b.getCall(0); + var aId = aCall && aCall.callId || -1; + var bId = bCall && bCall.callId || -1; + + return aId < bId ? -1 : 1; + }); + }, + + log: function () {}, + + logError: function (label, err) { + var msg = label + " threw exception: " + sinon.log(msg + "[" + err.name + "] " + err.message); + if (err.stack) { sinon.log(err.stack); } + + setTimeout(function () { + err.message = msg + err.message; + throw err; + }, 0); + }, + + typeOf: function (value) { + if (value === null) { + return "null"; + } + else if (value === undefined) { + return "undefined"; + } + var string = Object.prototype.toString.call(value); + return string.substring(8, string.length - 1).toLowerCase(); + }, + + createStubInstance: function (constructor) { + if (typeof constructor !== "function") { + throw new TypeError("The constructor should be a function."); + } + return sinon.stub(sinon.create(constructor.prototype)); + }, + + restore: function (object) { + if (object !== null && typeof object === "object") { + for (var prop in object) { + if (isRestorable(object[prop])) { + object[prop].restore(); + } + } + } + else if (isRestorable(object)) { + object.restore(); + } + } + }; + + var isNode = typeof module == "object" && typeof require == "function"; + + if (isNode) { + try { + buster = { format: require("buster-format") }; + } catch (e) {} + module.exports = sinon; + module.exports.spy = require("./sinon/spy"); + module.exports.spyCall = require("./sinon/call"); + module.exports.stub = require("./sinon/stub"); + module.exports.mock = require("./sinon/mock"); + module.exports.collection = require("./sinon/collection"); + module.exports.assert = require("./sinon/assert"); + module.exports.sandbox = require("./sinon/sandbox"); + module.exports.test = require("./sinon/test"); + module.exports.testCase = require("./sinon/test_case"); + module.exports.assert = require("./sinon/assert"); + module.exports.match = require("./sinon/match"); + } + + if (buster) { + var formatter = sinon.create(buster.format); + formatter.quoteStrings = false; + sinon.format = function () { + return formatter.ascii.apply(formatter, arguments); + }; + } else if (isNode) { + try { + var util = require("util"); + sinon.format = function (value) { + return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value; + }; + } catch (e) { + /* Node, but no util module - would be very old, but better safe than + sorry */ + } + } + + return sinon; +}(typeof buster == "object" && buster)); + +/* @depend ../sinon.js */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Match functions + * + * @author Maximilian Antoni (mail@maxantoni.de) + * @license BSD + * + * Copyright (c) 2012 Maximilian Antoni + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function assertType(value, type, name) { + var actual = sinon.typeOf(value); + if (actual !== type) { + throw new TypeError("Expected type of " + name + " to be " + + type + ", but was " + actual); + } + } + + var matcher = { + toString: function () { + return this.message; + } + }; + + function isMatcher(object) { + return matcher.isPrototypeOf(object); + } + + function matchObject(expectation, actual) { + if (actual === null || actual === undefined) { + return false; + } + for (var key in expectation) { + if (expectation.hasOwnProperty(key)) { + var exp = expectation[key]; + var act = actual[key]; + if (match.isMatcher(exp)) { + if (!exp.test(act)) { + return false; + } + } else if (sinon.typeOf(exp) === "object") { + if (!matchObject(exp, act)) { + return false; + } + } else if (!sinon.deepEqual(exp, act)) { + return false; + } + } + } + return true; + } + + matcher.or = function (m2) { + if (!isMatcher(m2)) { + throw new TypeError("Matcher expected"); + } + var m1 = this; + var or = sinon.create(matcher); + or.test = function (actual) { + return m1.test(actual) || m2.test(actual); + }; + or.message = m1.message + ".or(" + m2.message + ")"; + return or; + }; + + matcher.and = function (m2) { + if (!isMatcher(m2)) { + throw new TypeError("Matcher expected"); + } + var m1 = this; + var and = sinon.create(matcher); + and.test = function (actual) { + return m1.test(actual) && m2.test(actual); + }; + and.message = m1.message + ".and(" + m2.message + ")"; + return and; + }; + + var match = function (expectation, message) { + var m = sinon.create(matcher); + var type = sinon.typeOf(expectation); + switch (type) { + case "object": + if (typeof expectation.test === "function") { + m.test = function (actual) { + return expectation.test(actual) === true; + }; + m.message = "match(" + sinon.functionName(expectation.test) + ")"; + return m; + } + var str = []; + for (var key in expectation) { + if (expectation.hasOwnProperty(key)) { + str.push(key + ": " + expectation[key]); + } + } + m.test = function (actual) { + return matchObject(expectation, actual); + }; + m.message = "match(" + str.join(", ") + ")"; + break; + case "number": + m.test = function (actual) { + return expectation == actual; + }; + break; + case "string": + m.test = function (actual) { + if (typeof actual !== "string") { + return false; + } + return actual.indexOf(expectation) !== -1; + }; + m.message = "match(\"" + expectation + "\")"; + break; + case "regexp": + m.test = function (actual) { + if (typeof actual !== "string") { + return false; + } + return expectation.test(actual); + }; + break; + case "function": + m.test = expectation; + if (message) { + m.message = message; + } else { + m.message = "match(" + sinon.functionName(expectation) + ")"; + } + break; + default: + m.test = function (actual) { + return sinon.deepEqual(expectation, actual); + }; + } + if (!m.message) { + m.message = "match(" + expectation + ")"; + } + return m; + }; + + match.isMatcher = isMatcher; + + match.any = match(function () { + return true; + }, "any"); + + match.defined = match(function (actual) { + return actual !== null && actual !== undefined; + }, "defined"); + + match.truthy = match(function (actual) { + return !!actual; + }, "truthy"); + + match.falsy = match(function (actual) { + return !actual; + }, "falsy"); + + match.same = function (expectation) { + return match(function (actual) { + return expectation === actual; + }, "same(" + expectation + ")"); + }; + + match.typeOf = function (type) { + assertType(type, "string", "type"); + return match(function (actual) { + return sinon.typeOf(actual) === type; + }, "typeOf(\"" + type + "\")"); + }; + + match.instanceOf = function (type) { + assertType(type, "function", "type"); + return match(function (actual) { + return actual instanceof type; + }, "instanceOf(" + sinon.functionName(type) + ")"); + }; + + function createPropertyMatcher(propertyTest, messagePrefix) { + return function (property, value) { + assertType(property, "string", "property"); + var onlyProperty = arguments.length === 1; + var message = messagePrefix + "(\"" + property + "\""; + if (!onlyProperty) { + message += ", " + value; + } + message += ")"; + return match(function (actual) { + if (actual === undefined || actual === null || + !propertyTest(actual, property)) { + return false; + } + return onlyProperty || sinon.deepEqual(value, actual[property]); + }, message); + }; + } + + match.has = createPropertyMatcher(function (actual, property) { + if (typeof actual === "object") { + return property in actual; + } + return actual[property] !== undefined; + }, "has"); + + match.hasOwn = createPropertyMatcher(function (actual, property) { + return actual.hasOwnProperty(property); + }, "hasOwn"); + + match.bool = match.typeOf("boolean"); + match.number = match.typeOf("number"); + match.string = match.typeOf("string"); + match.object = match.typeOf("object"); + match.func = match.typeOf("function"); + match.array = match.typeOf("array"); + match.regexp = match.typeOf("regexp"); + match.date = match.typeOf("date"); + + if (commonJSModule) { + module.exports = match; + } else { + sinon.match = match; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend match.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Spy calls + * + * @author Christian Johansen (christian@cjohansen.no) + * @author Maximilian Antoni (mail@maxantoni.de) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + * Copyright (c) 2013 Maximilian Antoni + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function throwYieldError(proxy, text, args) { + var msg = sinon.functionName(proxy) + text; + if (args.length) { + msg += " Received [" + slice.call(args).join(", ") + "]"; + } + throw new Error(msg); + } + + var slice = Array.prototype.slice; + + var callProto = { + calledOn: function calledOn(thisValue) { + if (sinon.match && sinon.match.isMatcher(thisValue)) { + return thisValue.test(this.thisValue); + } + return this.thisValue === thisValue; + }, + + calledWith: function calledWith() { + for (var i = 0, l = arguments.length; i < l; i += 1) { + if (!sinon.deepEqual(arguments[i], this.args[i])) { + return false; + } + } + + return true; + }, + + calledWithMatch: function calledWithMatch() { + for (var i = 0, l = arguments.length; i < l; i += 1) { + var actual = this.args[i]; + var expectation = arguments[i]; + if (!sinon.match || !sinon.match(expectation).test(actual)) { + return false; + } + } + return true; + }, + + calledWithExactly: function calledWithExactly() { + return arguments.length == this.args.length && + this.calledWith.apply(this, arguments); + }, + + notCalledWith: function notCalledWith() { + return !this.calledWith.apply(this, arguments); + }, + + notCalledWithMatch: function notCalledWithMatch() { + return !this.calledWithMatch.apply(this, arguments); + }, + + returned: function returned(value) { + return sinon.deepEqual(value, this.returnValue); + }, + + threw: function threw(error) { + if (typeof error === "undefined" || !this.exception) { + return !!this.exception; + } + + return this.exception === error || this.exception.name === error; + }, + + calledWithNew: function calledWithNew(thisValue) { + return this.thisValue instanceof this.proxy; + }, + + calledBefore: function (other) { + return this.callId < other.callId; + }, + + calledAfter: function (other) { + return this.callId > other.callId; + }, + + callArg: function (pos) { + this.args[pos](); + }, + + callArgOn: function (pos, thisValue) { + this.args[pos].apply(thisValue); + }, + + callArgWith: function (pos) { + this.callArgOnWith.apply(this, [pos, null].concat(slice.call(arguments, 1))); + }, + + callArgOnWith: function (pos, thisValue) { + var args = slice.call(arguments, 2); + this.args[pos].apply(thisValue, args); + }, + + "yield": function () { + this.yieldOn.apply(this, [null].concat(slice.call(arguments, 0))); + }, + + yieldOn: function (thisValue) { + var args = this.args; + for (var i = 0, l = args.length; i < l; ++i) { + if (typeof args[i] === "function") { + args[i].apply(thisValue, slice.call(arguments, 1)); + return; + } + } + throwYieldError(this.proxy, " cannot yield since no callback was passed.", args); + }, + + yieldTo: function (prop) { + this.yieldToOn.apply(this, [prop, null].concat(slice.call(arguments, 1))); + }, + + yieldToOn: function (prop, thisValue) { + var args = this.args; + for (var i = 0, l = args.length; i < l; ++i) { + if (args[i] && typeof args[i][prop] === "function") { + args[i][prop].apply(thisValue, slice.call(arguments, 2)); + return; + } + } + throwYieldError(this.proxy, " cannot yield to '" + prop + + "' since no callback was passed.", args); + }, + + toString: function () { + var callStr = this.proxy.toString() + "("; + var args = []; + + for (var i = 0, l = this.args.length; i < l; ++i) { + args.push(sinon.format(this.args[i])); + } + + callStr = callStr + args.join(", ") + ")"; + + if (typeof this.returnValue != "undefined") { + callStr += " => " + sinon.format(this.returnValue); + } + + if (this.exception) { + callStr += " !" + this.exception.name; + + if (this.exception.message) { + callStr += "(" + this.exception.message + ")"; + } + } + + return callStr; + } + }; + + callProto.invokeCallback = callProto.yield; + + function createSpyCall(spy, thisValue, args, returnValue, exception, id) { + if (typeof id !== "number") { + throw new TypeError("Call id is not a number"); + } + var proxyCall = sinon.create(callProto); + proxyCall.proxy = spy; + proxyCall.thisValue = thisValue; + proxyCall.args = args; + proxyCall.returnValue = returnValue; + proxyCall.exception = exception; + proxyCall.callId = id; + + return proxyCall; + }; + createSpyCall.toString = callProto.toString; // used by mocks + + if (commonJSModule) { + module.exports = createSpyCall; + } else { + sinon.spyCall = createSpyCall; + } +}(typeof sinon == "object" && sinon || null)); + + +/** + * @depend ../sinon.js + * @depend call.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Spy functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + var push = Array.prototype.push; + var slice = Array.prototype.slice; + var callId = 0; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function spy(object, property) { + if (!property && typeof object == "function") { + return spy.create(object); + } + + if (!object && !property) { + return spy.create(function () { }); + } + + var method = object[property]; + return sinon.wrapMethod(object, property, spy.create(method)); + } + + function matchingFake(fakes, args, strict) { + if (!fakes) { + return; + } + + var alen = args.length; + + for (var i = 0, l = fakes.length; i < l; i++) { + if (fakes[i].matches(args, strict)) { + return fakes[i]; + } + } + } + + function incrementCallCount() { + this.called = true; + this.callCount += 1; + this.notCalled = false; + this.calledOnce = this.callCount == 1; + this.calledTwice = this.callCount == 2; + this.calledThrice = this.callCount == 3; + } + + function createCallProperties() { + this.firstCall = this.getCall(0); + this.secondCall = this.getCall(1); + this.thirdCall = this.getCall(2); + this.lastCall = this.getCall(this.callCount - 1); + } + + var vars = "a,b,c,d,e,f,g,h,i,j,k,l"; + function createProxy(func) { + // Retain the function length: + var p; + if (func.length) { + eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) + + ") { return p.invoke(func, this, slice.call(arguments)); });"); + } + else { + p = function proxy() { + return p.invoke(func, this, slice.call(arguments)); + }; + } + return p; + } + + var uuid = 0; + + // Public API + var spyApi = { + reset: function () { + this.called = false; + this.notCalled = true; + this.calledOnce = false; + this.calledTwice = false; + this.calledThrice = false; + this.callCount = 0; + this.firstCall = null; + this.secondCall = null; + this.thirdCall = null; + this.lastCall = null; + this.args = []; + this.returnValues = []; + this.thisValues = []; + this.exceptions = []; + this.callIds = []; + if (this.fakes) { + for (var i = 0; i < this.fakes.length; i++) { + this.fakes[i].reset(); + } + } + }, + + create: function create(func) { + var name; + + if (typeof func != "function") { + func = function () { }; + } else { + name = sinon.functionName(func); + } + + var proxy = createProxy(func); + + sinon.extend(proxy, spy); + delete proxy.create; + sinon.extend(proxy, func); + + proxy.reset(); + proxy.prototype = func.prototype; + proxy.displayName = name || "spy"; + proxy.toString = sinon.functionToString; + proxy._create = sinon.spy.create; + proxy.id = "spy#" + uuid++; + + return proxy; + }, + + invoke: function invoke(func, thisValue, args) { + var matching = matchingFake(this.fakes, args); + var exception, returnValue; + + incrementCallCount.call(this); + push.call(this.thisValues, thisValue); + push.call(this.args, args); + push.call(this.callIds, callId++); + + try { + if (matching) { + returnValue = matching.invoke(func, thisValue, args); + } else { + returnValue = (this.func || func).apply(thisValue, args); + } + } catch (e) { + push.call(this.returnValues, undefined); + exception = e; + throw e; + } finally { + push.call(this.exceptions, exception); + } + + push.call(this.returnValues, returnValue); + + createCallProperties.call(this); + + return returnValue; + }, + + getCall: function getCall(i) { + if (i < 0 || i >= this.callCount) { + return null; + } + + return sinon.spyCall(this, this.thisValues[i], this.args[i], + this.returnValues[i], this.exceptions[i], + this.callIds[i]); + }, + + calledBefore: function calledBefore(spyFn) { + if (!this.called) { + return false; + } + + if (!spyFn.called) { + return true; + } + + return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1]; + }, + + calledAfter: function calledAfter(spyFn) { + if (!this.called || !spyFn.called) { + return false; + } + + return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; + }, + + withArgs: function () { + var args = slice.call(arguments); + + if (this.fakes) { + var match = matchingFake(this.fakes, args, true); + + if (match) { + return match; + } + } else { + this.fakes = []; + } + + var original = this; + var fake = this._create(); + fake.matchingAguments = args; + push.call(this.fakes, fake); + + fake.withArgs = function () { + return original.withArgs.apply(original, arguments); + }; + + for (var i = 0; i < this.args.length; i++) { + if (fake.matches(this.args[i])) { + incrementCallCount.call(fake); + push.call(fake.thisValues, this.thisValues[i]); + push.call(fake.args, this.args[i]); + push.call(fake.returnValues, this.returnValues[i]); + push.call(fake.exceptions, this.exceptions[i]); + push.call(fake.callIds, this.callIds[i]); + } + } + createCallProperties.call(fake); + + return fake; + }, + + matches: function (args, strict) { + var margs = this.matchingAguments; + + if (margs.length <= args.length && + sinon.deepEqual(margs, args.slice(0, margs.length))) { + return !strict || margs.length == args.length; + } + }, + + printf: function (format) { + var spy = this; + var args = slice.call(arguments, 1); + var formatter; + + return (format || "").replace(/%(.)/g, function (match, specifyer) { + formatter = spyApi.formatters[specifyer]; + + if (typeof formatter == "function") { + return formatter.call(null, spy, args); + } else if (!isNaN(parseInt(specifyer), 10)) { + return sinon.format(args[specifyer - 1]); + } + + return "%" + specifyer; + }); + } + }; + + function delegateToCalls(method, matchAny, actual, notCalled) { + spyApi[method] = function () { + if (!this.called) { + if (notCalled) { + return notCalled.apply(this, arguments); + } + return false; + } + + var currentCall; + var matches = 0; + + for (var i = 0, l = this.callCount; i < l; i += 1) { + currentCall = this.getCall(i); + + if (currentCall[actual || method].apply(currentCall, arguments)) { + matches += 1; + + if (matchAny) { + return true; + } + } + } + + return matches === this.callCount; + }; + } + + delegateToCalls("calledOn", true); + delegateToCalls("alwaysCalledOn", false, "calledOn"); + delegateToCalls("calledWith", true); + delegateToCalls("calledWithMatch", true); + delegateToCalls("alwaysCalledWith", false, "calledWith"); + delegateToCalls("alwaysCalledWithMatch", false, "calledWithMatch"); + delegateToCalls("calledWithExactly", true); + delegateToCalls("alwaysCalledWithExactly", false, "calledWithExactly"); + delegateToCalls("neverCalledWith", false, "notCalledWith", + function () { return true; }); + delegateToCalls("neverCalledWithMatch", false, "notCalledWithMatch", + function () { return true; }); + delegateToCalls("threw", true); + delegateToCalls("alwaysThrew", false, "threw"); + delegateToCalls("returned", true); + delegateToCalls("alwaysReturned", false, "returned"); + delegateToCalls("calledWithNew", true); + delegateToCalls("alwaysCalledWithNew", false, "calledWithNew"); + delegateToCalls("callArg", false, "callArgWith", function () { + throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); + }); + spyApi.callArgWith = spyApi.callArg; + delegateToCalls("callArgOn", false, "callArgOnWith", function () { + throw new Error(this.toString() + " cannot call arg since it was not yet invoked."); + }); + spyApi.callArgOnWith = spyApi.callArgOn; + delegateToCalls("yield", false, "yield", function () { + throw new Error(this.toString() + " cannot yield since it was not yet invoked."); + }); + // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode. + spyApi.invokeCallback = spyApi.yield; + delegateToCalls("yieldOn", false, "yieldOn", function () { + throw new Error(this.toString() + " cannot yield since it was not yet invoked."); + }); + delegateToCalls("yieldTo", false, "yieldTo", function (property) { + throw new Error(this.toString() + " cannot yield to '" + property + + "' since it was not yet invoked."); + }); + delegateToCalls("yieldToOn", false, "yieldToOn", function (property) { + throw new Error(this.toString() + " cannot yield to '" + property + + "' since it was not yet invoked."); + }); + + spyApi.formatters = { + "c": function (spy) { + return sinon.timesInWords(spy.callCount); + }, + + "n": function (spy) { + return spy.toString(); + }, + + "C": function (spy) { + var calls = []; + + for (var i = 0, l = spy.callCount; i < l; ++i) { + var stringifiedCall = " " + spy.getCall(i).toString(); + if (/\n/.test(calls[i - 1])) { + stringifiedCall = "\n" + stringifiedCall; + } + push.call(calls, stringifiedCall); + } + + return calls.length > 0 ? "\n" + calls.join("\n") : ""; + }, + + "t": function (spy) { + var objects = []; + + for (var i = 0, l = spy.callCount; i < l; ++i) { + push.call(objects, sinon.format(spy.thisValues[i])); + } + + return objects.join(", "); + }, + + "*": function (spy, args) { + var formatted = []; + + for (var i = 0, l = args.length; i < l; ++i) { + push.call(formatted, sinon.format(args[i])); + } + + return formatted.join(", "); + } + }; + + sinon.extend(spy, spyApi); + + spy.spyCall = sinon.spyCall; + + if (commonJSModule) { + module.exports = spy; + } else { + sinon.spy = spy; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend spy.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global module, require, sinon*/ +/** + * Stub functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function stub(object, property, func) { + if (!!func && typeof func != "function") { + throw new TypeError("Custom stub should be function"); + } + + var wrapper; + + if (func) { + wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; + } else { + wrapper = stub.create(); + } + + if (!object && !property) { + return sinon.stub.create(); + } + + if (!property && !!object && typeof object == "object") { + for (var prop in object) { + if (typeof object[prop] === "function") { + stub(object, prop); + } + } + + return object; + } + + return sinon.wrapMethod(object, property, wrapper); + } + + function getChangingValue(stub, property) { + var index = stub.callCount - 1; + var values = stub[property]; + var prop = index in values ? values[index] : values[values.length - 1]; + stub[property + "Last"] = prop; + + return prop; + } + + function getCallback(stub, args) { + var callArgAt = getChangingValue(stub, "callArgAts"); + + if (callArgAt < 0) { + var callArgProp = getChangingValue(stub, "callArgProps"); + + for (var i = 0, l = args.length; i < l; ++i) { + if (!callArgProp && typeof args[i] == "function") { + return args[i]; + } + + if (callArgProp && args[i] && + typeof args[i][callArgProp] == "function") { + return args[i][callArgProp]; + } + } + + return null; + } + + return args[callArgAt]; + } + + var join = Array.prototype.join; + + function getCallbackError(stub, func, args) { + if (stub.callArgAtsLast < 0) { + var msg; + + if (stub.callArgPropsLast) { + msg = sinon.functionName(stub) + + " expected to yield to '" + stub.callArgPropsLast + + "', but no object with such a property was passed." + } else { + msg = sinon.functionName(stub) + + " expected to yield, but no callback was passed." + } + + if (args.length > 0) { + msg += " Received [" + join.call(args, ", ") + "]"; + } + + return msg; + } + + return "argument at index " + stub.callArgAtsLast + " is not a function: " + func; + } + + var nextTick = (function () { + if (typeof process === "object" && typeof process.nextTick === "function") { + return process.nextTick; + } else if (typeof setImmediate === "function") { + return setImmediate; + } else { + return function (callback) { + setTimeout(callback, 0); + }; + } + })(); + + function callCallback(stub, args) { + if (stub.callArgAts.length > 0) { + var func = getCallback(stub, args); + + if (typeof func != "function") { + throw new TypeError(getCallbackError(stub, func, args)); + } + + var callbackArguments = getChangingValue(stub, "callbackArguments"); + var callbackContext = getChangingValue(stub, "callbackContexts"); + + if (stub.callbackAsync) { + nextTick(function() { + func.apply(callbackContext, callbackArguments); + }); + } else { + func.apply(callbackContext, callbackArguments); + } + } + } + + var uuid = 0; + + sinon.extend(stub, (function () { + var slice = Array.prototype.slice, proto; + + function throwsException(error, message) { + if (typeof error == "string") { + this.exception = new Error(message || ""); + this.exception.name = error; + } else if (!error) { + this.exception = new Error("Error"); + } else { + this.exception = error; + } + + return this; + } + + proto = { + create: function create() { + var functionStub = function () { + + callCallback(functionStub, arguments); + + if (functionStub.exception) { + throw functionStub.exception; + } else if (typeof functionStub.returnArgAt == 'number') { + return arguments[functionStub.returnArgAt]; + } else if (functionStub.returnThis) { + return this; + } + return functionStub.returnValue; + }; + + functionStub.id = "stub#" + uuid++; + var orig = functionStub; + functionStub = sinon.spy.create(functionStub); + functionStub.func = orig; + + functionStub.callArgAts = []; + functionStub.callbackArguments = []; + functionStub.callbackContexts = []; + functionStub.callArgProps = []; + + sinon.extend(functionStub, stub); + functionStub._create = sinon.stub.create; + functionStub.displayName = "stub"; + functionStub.toString = sinon.functionToString; + + return functionStub; + }, + + resetBehavior: function () { + var i; + + this.callArgAts = []; + this.callbackArguments = []; + this.callbackContexts = []; + this.callArgProps = []; + + delete this.returnValue; + delete this.returnArgAt; + this.returnThis = false; + + if (this.fakes) { + for (i = 0; i < this.fakes.length; i++) { + this.fakes[i].resetBehavior(); + } + } + }, + + returns: function returns(value) { + this.returnValue = value; + + return this; + }, + + returnsArg: function returnsArg(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.returnArgAt = pos; + + return this; + }, + + returnsThis: function returnsThis() { + this.returnThis = true; + + return this; + }, + + "throws": throwsException, + throwsException: throwsException, + + callsArg: function callsArg(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.callArgAts.push(pos); + this.callbackArguments.push([]); + this.callbackContexts.push(undefined); + this.callArgProps.push(undefined); + + return this; + }, + + callsArgOn: function callsArgOn(pos, context) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAts.push(pos); + this.callbackArguments.push([]); + this.callbackContexts.push(context); + this.callArgProps.push(undefined); + + return this; + }, + + callsArgWith: function callsArgWith(pos) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + + this.callArgAts.push(pos); + this.callbackArguments.push(slice.call(arguments, 1)); + this.callbackContexts.push(undefined); + this.callArgProps.push(undefined); + + return this; + }, + + callsArgOnWith: function callsArgWith(pos, context) { + if (typeof pos != "number") { + throw new TypeError("argument index is not number"); + } + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAts.push(pos); + this.callbackArguments.push(slice.call(arguments, 2)); + this.callbackContexts.push(context); + this.callArgProps.push(undefined); + + return this; + }, + + yields: function () { + this.callArgAts.push(-1); + this.callbackArguments.push(slice.call(arguments, 0)); + this.callbackContexts.push(undefined); + this.callArgProps.push(undefined); + + return this; + }, + + yieldsOn: function (context) { + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAts.push(-1); + this.callbackArguments.push(slice.call(arguments, 1)); + this.callbackContexts.push(context); + this.callArgProps.push(undefined); + + return this; + }, + + yieldsTo: function (prop) { + this.callArgAts.push(-1); + this.callbackArguments.push(slice.call(arguments, 1)); + this.callbackContexts.push(undefined); + this.callArgProps.push(prop); + + return this; + }, + + yieldsToOn: function (prop, context) { + if (typeof context != "object") { + throw new TypeError("argument context is not an object"); + } + + this.callArgAts.push(-1); + this.callbackArguments.push(slice.call(arguments, 2)); + this.callbackContexts.push(context); + this.callArgProps.push(prop); + + return this; + } + }; + + // create asynchronous versions of callsArg* and yields* methods + for (var method in proto) { + // need to avoid creating anotherasync versions of the newly added async methods + if (proto.hasOwnProperty(method) && + method.match(/^(callsArg|yields|thenYields$)/) && + !method.match(/Async/)) { + proto[method + 'Async'] = (function (syncFnName) { + return function () { + this.callbackAsync = true; + return this[syncFnName].apply(this, arguments); + }; + })(method); + } + } + + return proto; + + }())); + + if (commonJSModule) { + module.exports = stub; + } else { + sinon.stub = stub; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + */ +/*jslint eqeqeq: false, onevar: false, nomen: false*/ +/*global module, require, sinon*/ +/** + * Mock functions. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + var push = [].push; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function mock(object) { + if (!object) { + return sinon.expectation.create("Anonymous mock"); + } + + return mock.create(object); + } + + sinon.mock = mock; + + sinon.extend(mock, (function () { + function each(collection, callback) { + if (!collection) { + return; + } + + for (var i = 0, l = collection.length; i < l; i += 1) { + callback(collection[i]); + } + } + + return { + create: function create(object) { + if (!object) { + throw new TypeError("object is null"); + } + + var mockObject = sinon.extend({}, mock); + mockObject.object = object; + delete mockObject.create; + + return mockObject; + }, + + expects: function expects(method) { + if (!method) { + throw new TypeError("method is falsy"); + } + + if (!this.expectations) { + this.expectations = {}; + this.proxies = []; + } + + if (!this.expectations[method]) { + this.expectations[method] = []; + var mockObject = this; + + sinon.wrapMethod(this.object, method, function () { + return mockObject.invokeMethod(method, this, arguments); + }); + + push.call(this.proxies, method); + } + + var expectation = sinon.expectation.create(method); + push.call(this.expectations[method], expectation); + + return expectation; + }, + + restore: function restore() { + var object = this.object; + + each(this.proxies, function (proxy) { + if (typeof object[proxy].restore == "function") { + object[proxy].restore(); + } + }); + }, + + verify: function verify() { + var expectations = this.expectations || {}; + var messages = [], met = []; + + each(this.proxies, function (proxy) { + each(expectations[proxy], function (expectation) { + if (!expectation.met()) { + push.call(messages, expectation.toString()); + } else { + push.call(met, expectation.toString()); + } + }); + }); + + this.restore(); + + if (messages.length > 0) { + sinon.expectation.fail(messages.concat(met).join("\n")); + } else { + sinon.expectation.pass(messages.concat(met).join("\n")); + } + + return true; + }, + + invokeMethod: function invokeMethod(method, thisValue, args) { + var expectations = this.expectations && this.expectations[method]; + var length = expectations && expectations.length || 0, i; + + for (i = 0; i < length; i += 1) { + if (!expectations[i].met() && + expectations[i].allowsCall(thisValue, args)) { + return expectations[i].apply(thisValue, args); + } + } + + var messages = [], available, exhausted = 0; + + for (i = 0; i < length; i += 1) { + if (expectations[i].allowsCall(thisValue, args)) { + available = available || expectations[i]; + } else { + exhausted += 1; + } + push.call(messages, " " + expectations[i].toString()); + } + + if (exhausted === 0) { + return available.apply(thisValue, args); + } + + messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ + proxy: method, + args: args + })); + + sinon.expectation.fail(messages.join("\n")); + } + }; + }())); + + var times = sinon.timesInWords; + + sinon.expectation = (function () { + var slice = Array.prototype.slice; + var _invoke = sinon.spy.invoke; + + function callCountInWords(callCount) { + if (callCount == 0) { + return "never called"; + } else { + return "called " + times(callCount); + } + } + + function expectedCallCountInWords(expectation) { + var min = expectation.minCalls; + var max = expectation.maxCalls; + + if (typeof min == "number" && typeof max == "number") { + var str = times(min); + + if (min != max) { + str = "at least " + str + " and at most " + times(max); + } + + return str; + } + + if (typeof min == "number") { + return "at least " + times(min); + } + + return "at most " + times(max); + } + + function receivedMinCalls(expectation) { + var hasMinLimit = typeof expectation.minCalls == "number"; + return !hasMinLimit || expectation.callCount >= expectation.minCalls; + } + + function receivedMaxCalls(expectation) { + if (typeof expectation.maxCalls != "number") { + return false; + } + + return expectation.callCount == expectation.maxCalls; + } + + return { + minCalls: 1, + maxCalls: 1, + + create: function create(methodName) { + var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); + delete expectation.create; + expectation.method = methodName; + + return expectation; + }, + + invoke: function invoke(func, thisValue, args) { + this.verifyCallAllowed(thisValue, args); + + return _invoke.apply(this, arguments); + }, + + atLeast: function atLeast(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not number"); + } + + if (!this.limitsSet) { + this.maxCalls = null; + this.limitsSet = true; + } + + this.minCalls = num; + + return this; + }, + + atMost: function atMost(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not number"); + } + + if (!this.limitsSet) { + this.minCalls = null; + this.limitsSet = true; + } + + this.maxCalls = num; + + return this; + }, + + never: function never() { + return this.exactly(0); + }, + + once: function once() { + return this.exactly(1); + }, + + twice: function twice() { + return this.exactly(2); + }, + + thrice: function thrice() { + return this.exactly(3); + }, + + exactly: function exactly(num) { + if (typeof num != "number") { + throw new TypeError("'" + num + "' is not a number"); + } + + this.atLeast(num); + return this.atMost(num); + }, + + met: function met() { + return !this.failed && receivedMinCalls(this); + }, + + verifyCallAllowed: function verifyCallAllowed(thisValue, args) { + if (receivedMaxCalls(this)) { + this.failed = true; + sinon.expectation.fail(this.method + " already called " + times(this.maxCalls)); + } + + if ("expectedThis" in this && this.expectedThis !== thisValue) { + sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " + + this.expectedThis); + } + + if (!("expectedArguments" in this)) { + return; + } + + if (!args) { + sinon.expectation.fail(this.method + " received no arguments, expected " + + sinon.format(this.expectedArguments)); + } + + if (args.length < this.expectedArguments.length) { + sinon.expectation.fail(this.method + " received too few arguments (" + sinon.format(args) + + "), expected " + sinon.format(this.expectedArguments)); + } + + if (this.expectsExactArgCount && + args.length != this.expectedArguments.length) { + sinon.expectation.fail(this.method + " received too many arguments (" + sinon.format(args) + + "), expected " + sinon.format(this.expectedArguments)); + } + + for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { + if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { + sinon.expectation.fail(this.method + " received wrong arguments " + sinon.format(args) + + ", expected " + sinon.format(this.expectedArguments)); + } + } + }, + + allowsCall: function allowsCall(thisValue, args) { + if (this.met() && receivedMaxCalls(this)) { + return false; + } + + if ("expectedThis" in this && this.expectedThis !== thisValue) { + return false; + } + + if (!("expectedArguments" in this)) { + return true; + } + + args = args || []; + + if (args.length < this.expectedArguments.length) { + return false; + } + + if (this.expectsExactArgCount && + args.length != this.expectedArguments.length) { + return false; + } + + for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { + if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { + return false; + } + } + + return true; + }, + + withArgs: function withArgs() { + this.expectedArguments = slice.call(arguments); + return this; + }, + + withExactArgs: function withExactArgs() { + this.withArgs.apply(this, arguments); + this.expectsExactArgCount = true; + return this; + }, + + on: function on(thisValue) { + this.expectedThis = thisValue; + return this; + }, + + toString: function () { + var args = (this.expectedArguments || []).slice(); + + if (!this.expectsExactArgCount) { + push.call(args, "[...]"); + } + + var callStr = sinon.spyCall.toString.call({ + proxy: this.method || "anonymous mock expectation", + args: args + }); + + var message = callStr.replace(", [...", "[, ...") + " " + + expectedCallCountInWords(this); + + if (this.met()) { + return "Expectation met: " + message; + } + + return "Expected " + message + " (" + + callCountInWords(this.callCount) + ")"; + }, + + verify: function verify() { + if (!this.met()) { + sinon.expectation.fail(this.toString()); + } else { + sinon.expectation.pass(this.toString()); + } + + return true; + }, + + pass: function(message) { + sinon.assert.pass(message); + }, + fail: function (message) { + var exception = new Error(message); + exception.name = "ExpectationError"; + + throw exception; + } + }; + }()); + + if (commonJSModule) { + module.exports = mock; + } else { + sinon.mock = mock; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + * @depend mock.js + */ +/*jslint eqeqeq: false, onevar: false, forin: true*/ +/*global module, require, sinon*/ +/** + * Collections of stubs, spies and mocks. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + var push = [].push; + var hasOwnProperty = Object.prototype.hasOwnProperty; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function getFakes(fakeCollection) { + if (!fakeCollection.fakes) { + fakeCollection.fakes = []; + } + + return fakeCollection.fakes; + } + + function each(fakeCollection, method) { + var fakes = getFakes(fakeCollection); + + for (var i = 0, l = fakes.length; i < l; i += 1) { + if (typeof fakes[i][method] == "function") { + fakes[i][method](); + } + } + } + + function compact(fakeCollection) { + var fakes = getFakes(fakeCollection); + var i = 0; + while (i < fakes.length) { + fakes.splice(i, 1); + } + } + + var collection = { + verify: function resolve() { + each(this, "verify"); + }, + + restore: function restore() { + each(this, "restore"); + compact(this); + }, + + verifyAndRestore: function verifyAndRestore() { + var exception; + + try { + this.verify(); + } catch (e) { + exception = e; + } + + this.restore(); + + if (exception) { + throw exception; + } + }, + + add: function add(fake) { + push.call(getFakes(this), fake); + return fake; + }, + + spy: function spy() { + return this.add(sinon.spy.apply(sinon, arguments)); + }, + + stub: function stub(object, property, value) { + if (property) { + var original = object[property]; + + if (typeof original != "function") { + if (!hasOwnProperty.call(object, property)) { + throw new TypeError("Cannot stub non-existent own property " + property); + } + + object[property] = value; + + return this.add({ + restore: function () { + object[property] = original; + } + }); + } + } + if (!property && !!object && typeof object == "object") { + var stubbedObj = sinon.stub.apply(sinon, arguments); + + for (var prop in stubbedObj) { + if (typeof stubbedObj[prop] === "function") { + this.add(stubbedObj[prop]); + } + } + + return stubbedObj; + } + + return this.add(sinon.stub.apply(sinon, arguments)); + }, + + mock: function mock() { + return this.add(sinon.mock.apply(sinon, arguments)); + }, + + inject: function inject(obj) { + var col = this; + + obj.spy = function () { + return col.spy.apply(col, arguments); + }; + + obj.stub = function () { + return col.stub.apply(col, arguments); + }; + + obj.mock = function () { + return col.mock.apply(col, arguments); + }; + + return obj; + } + }; + + if (commonJSModule) { + module.exports = collection; + } else { + sinon.collection = collection; + } +}(typeof sinon == "object" && sinon || null)); + +/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ +/*global module, require, window*/ +/** + * Fake timer API + * setTimeout + * setInterval + * clearTimeout + * clearInterval + * tick + * reset + * Date + * + * Inspired by jsUnitMockTimeOut from JsUnit + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof sinon == "undefined") { + var sinon = {}; +} + +(function (global) { + var id = 1; + + function addTimer(args, recurring) { + if (args.length === 0) { + throw new Error("Function requires at least 1 parameter"); + } + + var toId = id++; + var delay = args[1] || 0; + + if (!this.timeouts) { + this.timeouts = {}; + } + + this.timeouts[toId] = { + id: toId, + func: args[0], + callAt: this.now + delay, + invokeArgs: Array.prototype.slice.call(args, 2) + }; + + if (recurring === true) { + this.timeouts[toId].interval = delay; + } + + return toId; + } + + function parseTime(str) { + if (!str) { + return 0; + } + + var strings = str.split(":"); + var l = strings.length, i = l; + var ms = 0, parsed; + + if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { + throw new Error("tick only understands numbers and 'h:m:s'"); + } + + while (i--) { + parsed = parseInt(strings[i], 10); + + if (parsed >= 60) { + throw new Error("Invalid time " + str); + } + + ms += parsed * Math.pow(60, (l - i - 1)); + } + + return ms * 1000; + } + + function createObject(object) { + var newObject; + + if (Object.create) { + newObject = Object.create(object); + } else { + var F = function () {}; + F.prototype = object; + newObject = new F(); + } + + newObject.Date.clock = newObject; + return newObject; + } + + sinon.clock = { + now: 0, + + create: function create(now) { + var clock = createObject(this); + + if (typeof now == "number") { + clock.now = now; + } + + if (!!now && typeof now == "object") { + throw new TypeError("now should be milliseconds since UNIX epoch"); + } + + return clock; + }, + + setTimeout: function setTimeout(callback, timeout) { + return addTimer.call(this, arguments, false); + }, + + clearTimeout: function clearTimeout(timerId) { + if (!this.timeouts) { + this.timeouts = []; + } + + if (timerId in this.timeouts) { + delete this.timeouts[timerId]; + } + }, + + setInterval: function setInterval(callback, timeout) { + return addTimer.call(this, arguments, true); + }, + + clearInterval: function clearInterval(timerId) { + this.clearTimeout(timerId); + }, + + tick: function tick(ms) { + ms = typeof ms == "number" ? ms : parseTime(ms); + var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; + var timer = this.firstTimerInRange(tickFrom, tickTo); + + var firstException; + while (timer && tickFrom <= tickTo) { + if (this.timeouts[timer.id]) { + tickFrom = this.now = timer.callAt; + try { + this.callTimer(timer); + } catch (e) { + firstException = firstException || e; + } + } + + timer = this.firstTimerInRange(previous, tickTo); + previous = tickFrom; + } + + this.now = tickTo; + + if (firstException) { + throw firstException; + } + + return this.now; + }, + + firstTimerInRange: function (from, to) { + var timer, smallest, originalTimer; + + for (var id in this.timeouts) { + if (this.timeouts.hasOwnProperty(id)) { + if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { + continue; + } + + if (!smallest || this.timeouts[id].callAt < smallest) { + originalTimer = this.timeouts[id]; + smallest = this.timeouts[id].callAt; + + timer = { + func: this.timeouts[id].func, + callAt: this.timeouts[id].callAt, + interval: this.timeouts[id].interval, + id: this.timeouts[id].id, + invokeArgs: this.timeouts[id].invokeArgs + }; + } + } + } + + return timer || null; + }, + + callTimer: function (timer) { + if (typeof timer.interval == "number") { + this.timeouts[timer.id].callAt += timer.interval; + } else { + delete this.timeouts[timer.id]; + } + + try { + if (typeof timer.func == "function") { + timer.func.apply(null, timer.invokeArgs); + } else { + eval(timer.func); + } + } catch (e) { + var exception = e; + } + + if (!this.timeouts[timer.id]) { + if (exception) { + throw exception; + } + return; + } + + if (exception) { + throw exception; + } + }, + + reset: function reset() { + this.timeouts = {}; + }, + + Date: (function () { + var NativeDate = Date; + + function ClockDate(year, month, date, hour, minute, second, ms) { + // Defensive and verbose to avoid potential harm in passing + // explicit undefined when user does not pass argument + switch (arguments.length) { + case 0: + return new NativeDate(ClockDate.clock.now); + case 1: + return new NativeDate(year); + case 2: + return new NativeDate(year, month); + case 3: + return new NativeDate(year, month, date); + case 4: + return new NativeDate(year, month, date, hour); + case 5: + return new NativeDate(year, month, date, hour, minute); + case 6: + return new NativeDate(year, month, date, hour, minute, second); + default: + return new NativeDate(year, month, date, hour, minute, second, ms); + } + } + + return mirrorDateProperties(ClockDate, NativeDate); + }()) + }; + + function mirrorDateProperties(target, source) { + if (source.now) { + target.now = function now() { + return target.clock.now; + }; + } else { + delete target.now; + } + + if (source.toSource) { + target.toSource = function toSource() { + return source.toSource(); + }; + } else { + delete target.toSource; + } + + target.toString = function toString() { + return source.toString(); + }; + + target.prototype = source.prototype; + target.parse = source.parse; + target.UTC = source.UTC; + target.prototype.toUTCString = source.prototype.toUTCString; + return target; + } + + var methods = ["Date", "setTimeout", "setInterval", + "clearTimeout", "clearInterval"]; + + function restore() { + var method; + + for (var i = 0, l = this.methods.length; i < l; i++) { + method = this.methods[i]; + if (global[method].hadOwnProperty) { + global[method] = this["_" + method]; + } else { + delete global[method]; + } + } + + // Prevent multiple executions which will completely remove these props + this.methods = []; + } + + function stubGlobal(method, clock) { + clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method); + clock["_" + method] = global[method]; + + if (method == "Date") { + var date = mirrorDateProperties(clock[method], global[method]); + global[method] = date; + } else { + global[method] = function () { + return clock[method].apply(clock, arguments); + }; + + for (var prop in clock[method]) { + if (clock[method].hasOwnProperty(prop)) { + global[method][prop] = clock[method][prop]; + } + } + } + + global[method].clock = clock; + } + + sinon.useFakeTimers = function useFakeTimers(now) { + var clock = sinon.clock.create(now); + clock.restore = restore; + clock.methods = Array.prototype.slice.call(arguments, + typeof now == "number" ? 1 : 0); + + if (clock.methods.length === 0) { + clock.methods = methods; + } + + for (var i = 0, l = clock.methods.length; i < l; i++) { + stubGlobal(clock.methods[i], clock); + } + + return clock; + }; +}(typeof global != "undefined" && typeof global !== "function" ? global : this)); + +sinon.timers = { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + Date: Date +}; + +if (typeof module == "object" && typeof require == "function") { + module.exports = sinon; +} + +/*jslint eqeqeq: false, onevar: false*/ +/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ +/** + * Minimal Event interface implementation + * + * Original implementation by Sven Fuchs: https://gist.github.com/995028 + * Modifications and tests by Christian Johansen. + * + * @author Sven Fuchs (svenfuchs@artweb-design.de) + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2011 Sven Fuchs, Christian Johansen + */ + +if (typeof sinon == "undefined") { + this.sinon = {}; +} + +(function () { + var push = [].push; + + sinon.Event = function Event(type, bubbles, cancelable, target) { + this.initEvent(type, bubbles, cancelable, target); + }; + + sinon.Event.prototype = { + initEvent: function(type, bubbles, cancelable, target) { + this.type = type; + this.bubbles = bubbles; + this.cancelable = cancelable; + this.target = target; + }, + + stopPropagation: function () {}, + + preventDefault: function () { + this.defaultPrevented = true; + } + }; + + sinon.EventTarget = { + addEventListener: function addEventListener(event, listener, useCapture) { + this.eventListeners = this.eventListeners || {}; + this.eventListeners[event] = this.eventListeners[event] || []; + push.call(this.eventListeners[event], listener); + }, + + removeEventListener: function removeEventListener(event, listener, useCapture) { + var listeners = this.eventListeners && this.eventListeners[event] || []; + + for (var i = 0, l = listeners.length; i < l; ++i) { + if (listeners[i] == listener) { + return listeners.splice(i, 1); + } + } + }, + + dispatchEvent: function dispatchEvent(event) { + var type = event.type; + var listeners = this.eventListeners && this.eventListeners[type] || []; + + for (var i = 0; i < listeners.length; i++) { + if (typeof listeners[i] == "function") { + listeners[i].call(this, event); + } else { + listeners[i].handleEvent(event); + } + } + + return !!event.defaultPrevented; + } + }; +}()); + +/** + * @depend ../../sinon.js + * @depend event.js + */ +/*jslint eqeqeq: false, onevar: false*/ +/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ +/** + * Fake XMLHttpRequest object + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof sinon == "undefined") { + this.sinon = {}; +} +sinon.xhr = { XMLHttpRequest: this.XMLHttpRequest }; + +// wrapper for global +(function(global) { + var xhr = sinon.xhr; + xhr.GlobalXMLHttpRequest = global.XMLHttpRequest; + xhr.GlobalActiveXObject = global.ActiveXObject; + xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined"; + xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined"; + xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX + ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false; + + /*jsl:ignore*/ + var unsafeHeaders = { + "Accept-Charset": true, + "Accept-Encoding": true, + "Connection": true, + "Content-Length": true, + "Cookie": true, + "Cookie2": true, + "Content-Transfer-Encoding": true, + "Date": true, + "Expect": true, + "Host": true, + "Keep-Alive": true, + "Referer": true, + "TE": true, + "Trailer": true, + "Transfer-Encoding": true, + "Upgrade": true, + "User-Agent": true, + "Via": true + }; + /*jsl:end*/ + + function FakeXMLHttpRequest() { + this.readyState = FakeXMLHttpRequest.UNSENT; + this.requestHeaders = {}; + this.requestBody = null; + this.status = 0; + this.statusText = ""; + + var xhr = this; + + ["loadstart", "load", "abort", "loadend"].forEach(function (eventName) { + xhr.addEventListener(eventName, function (event) { + var listener = xhr["on" + eventName]; + + if (listener && typeof listener == "function") { + listener(event); + } + }); + }); + + if (typeof FakeXMLHttpRequest.onCreate == "function") { + FakeXMLHttpRequest.onCreate(this); + } + } + + function verifyState(xhr) { + if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { + throw new Error("INVALID_STATE_ERR"); + } + + if (xhr.sendFlag) { + throw new Error("INVALID_STATE_ERR"); + } + } + + // filtering to enable a white-list version of Sinon FakeXhr, + // where whitelisted requests are passed through to real XHR + function each(collection, callback) { + if (!collection) return; + for (var i = 0, l = collection.length; i < l; i += 1) { + callback(collection[i]); + } + } + function some(collection, callback) { + for (var index = 0; index < collection.length; index++) { + if(callback(collection[index]) === true) return true; + }; + return false; + } + // largest arity in XHR is 5 - XHR#open + var apply = function(obj,method,args) { + switch(args.length) { + case 0: return obj[method](); + case 1: return obj[method](args[0]); + case 2: return obj[method](args[0],args[1]); + case 3: return obj[method](args[0],args[1],args[2]); + case 4: return obj[method](args[0],args[1],args[2],args[3]); + case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]); + }; + }; + + FakeXMLHttpRequest.filters = []; + FakeXMLHttpRequest.addFilter = function(fn) { + this.filters.push(fn) + }; + var IE6Re = /MSIE 6/; + FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) { + var xhr = new sinon.xhr.workingXHR(); + each(["open","setRequestHeader","send","abort","getResponseHeader", + "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"], + function(method) { + fakeXhr[method] = function() { + return apply(xhr,method,arguments); + }; + }); + + var copyAttrs = function(args) { + each(args, function(attr) { + try { + fakeXhr[attr] = xhr[attr] + } catch(e) { + if(!IE6Re.test(navigator.userAgent)) throw e; + } + }); + }; + + var stateChange = function() { + fakeXhr.readyState = xhr.readyState; + if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) { + copyAttrs(["status","statusText"]); + } + if(xhr.readyState >= FakeXMLHttpRequest.LOADING) { + copyAttrs(["responseText"]); + } + if(xhr.readyState === FakeXMLHttpRequest.DONE) { + copyAttrs(["responseXML"]); + } + if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr); + }; + if(xhr.addEventListener) { + for(var event in fakeXhr.eventListeners) { + if(fakeXhr.eventListeners.hasOwnProperty(event)) { + each(fakeXhr.eventListeners[event],function(handler) { + xhr.addEventListener(event, handler); + }); + } + } + xhr.addEventListener("readystatechange",stateChange); + } else { + xhr.onreadystatechange = stateChange; + } + apply(xhr,"open",xhrArgs); + }; + FakeXMLHttpRequest.useFilters = false; + + function verifyRequestSent(xhr) { + if (xhr.readyState == FakeXMLHttpRequest.DONE) { + throw new Error("Request done"); + } + } + + function verifyHeadersReceived(xhr) { + if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { + throw new Error("No headers received"); + } + } + + function verifyResponseBodyType(body) { + if (typeof body != "string") { + var error = new Error("Attempted to respond to fake XMLHttpRequest with " + + body + ", which is not a string."); + error.name = "InvalidBodyException"; + throw error; + } + } + + sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { + async: true, + + open: function open(method, url, async, username, password) { + this.method = method; + this.url = url; + this.async = typeof async == "boolean" ? async : true; + this.username = username; + this.password = password; + this.responseText = null; + this.responseXML = null; + this.requestHeaders = {}; + this.sendFlag = false; + if(sinon.FakeXMLHttpRequest.useFilters === true) { + var xhrArgs = arguments; + var defake = some(FakeXMLHttpRequest.filters,function(filter) { + return filter.apply(this,xhrArgs) + }); + if (defake) { + return sinon.FakeXMLHttpRequest.defake(this,arguments); + } + } + this.readyStateChange(FakeXMLHttpRequest.OPENED); + }, + + readyStateChange: function readyStateChange(state) { + this.readyState = state; + + if (typeof this.onreadystatechange == "function") { + try { + this.onreadystatechange(); + } catch (e) { + sinon.logError("Fake XHR onreadystatechange handler", e); + } + } + + this.dispatchEvent(new sinon.Event("readystatechange")); + + switch (this.readyState) { + case FakeXMLHttpRequest.DONE: + this.dispatchEvent(new sinon.Event("load", false, false, this)); + this.dispatchEvent(new sinon.Event("loadend", false, false, this)); + break; + } + }, + + setRequestHeader: function setRequestHeader(header, value) { + verifyState(this); + + if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { + throw new Error("Refused to set unsafe header \"" + header + "\""); + } + + if (this.requestHeaders[header]) { + this.requestHeaders[header] += "," + value; + } else { + this.requestHeaders[header] = value; + } + }, + + // Helps testing + setResponseHeaders: function setResponseHeaders(headers) { + this.responseHeaders = {}; + + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + this.responseHeaders[header] = headers[header]; + } + } + + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); + } else { + this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; + } + }, + + // Currently treats ALL data as a DOMString (i.e. no Document) + send: function send(data) { + verifyState(this); + + if (!/^(get|head)$/i.test(this.method)) { + if (this.requestHeaders["Content-Type"]) { + var value = this.requestHeaders["Content-Type"].split(";"); + this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; + } else { + this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; + } + + this.requestBody = data; + } + + this.errorFlag = false; + this.sendFlag = this.async; + this.readyStateChange(FakeXMLHttpRequest.OPENED); + + if (typeof this.onSend == "function") { + this.onSend(this); + } + + this.dispatchEvent(new sinon.Event("loadstart", false, false, this)); + }, + + abort: function abort() { + this.aborted = true; + this.responseText = null; + this.errorFlag = true; + this.requestHeaders = {}; + + if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { + this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); + this.sendFlag = false; + } + + this.readyState = sinon.FakeXMLHttpRequest.UNSENT; + + this.dispatchEvent(new sinon.Event("abort", false, false, this)); + if (typeof this.onerror === "function") { + this.onerror(); + } + }, + + getResponseHeader: function getResponseHeader(header) { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return null; + } + + if (/^Set-Cookie2?$/i.test(header)) { + return null; + } + + header = header.toLowerCase(); + + for (var h in this.responseHeaders) { + if (h.toLowerCase() == header) { + return this.responseHeaders[h]; + } + } + + return null; + }, + + getAllResponseHeaders: function getAllResponseHeaders() { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return ""; + } + + var headers = ""; + + for (var header in this.responseHeaders) { + if (this.responseHeaders.hasOwnProperty(header) && + !/^Set-Cookie2?$/i.test(header)) { + headers += header + ": " + this.responseHeaders[header] + "\r\n"; + } + } + + return headers; + }, + + setResponseBody: function setResponseBody(body) { + verifyRequestSent(this); + verifyHeadersReceived(this); + verifyResponseBodyType(body); + + var chunkSize = this.chunkSize || 10; + var index = 0; + this.responseText = ""; + + do { + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.LOADING); + } + + this.responseText += body.substring(index, index + chunkSize); + index += chunkSize; + } while (index < body.length); + + var type = this.getResponseHeader("Content-Type"); + + if (this.responseText && + (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { + try { + this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); + } catch (e) { + // Unable to parse XML - no biggie + } + } + + if (this.async) { + this.readyStateChange(FakeXMLHttpRequest.DONE); + } else { + this.readyState = FakeXMLHttpRequest.DONE; + } + }, + + respond: function respond(status, headers, body) { + this.setResponseHeaders(headers || {}); + this.status = typeof status == "number" ? status : 200; + this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; + this.setResponseBody(body || ""); + if (typeof this.onload === "function"){ + this.onload(); + } + + } + }); + + sinon.extend(FakeXMLHttpRequest, { + UNSENT: 0, + OPENED: 1, + HEADERS_RECEIVED: 2, + LOADING: 3, + DONE: 4 + }); + + // Borrowed from JSpec + FakeXMLHttpRequest.parseXML = function parseXML(text) { + var xmlDoc; + + if (typeof DOMParser != "undefined") { + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(text, "text/xml"); + } else { + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(text); + } + + return xmlDoc; + }; + + FakeXMLHttpRequest.statusCodes = { + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choice", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 422: "Unprocessable Entity", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported" + }; + + sinon.useFakeXMLHttpRequest = function () { + sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { + if (xhr.supportsXHR) { + global.XMLHttpRequest = xhr.GlobalXMLHttpRequest; + } + + if (xhr.supportsActiveX) { + global.ActiveXObject = xhr.GlobalActiveXObject; + } + + delete sinon.FakeXMLHttpRequest.restore; + + if (keepOnCreate !== true) { + delete sinon.FakeXMLHttpRequest.onCreate; + } + }; + if (xhr.supportsXHR) { + global.XMLHttpRequest = sinon.FakeXMLHttpRequest; + } + + if (xhr.supportsActiveX) { + global.ActiveXObject = function ActiveXObject(objId) { + if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { + + return new sinon.FakeXMLHttpRequest(); + } + + return new xhr.GlobalActiveXObject(objId); + }; + } + + return sinon.FakeXMLHttpRequest; + }; + + sinon.FakeXMLHttpRequest = FakeXMLHttpRequest; +})(this); + +if (typeof module == "object" && typeof require == "function") { + module.exports = sinon; +} + +/** + * @depend fake_xml_http_request.js + */ +/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ +/*global module, require, window*/ +/** + * The Sinon "server" mimics a web server that receives requests from + * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, + * both synchronously and asynchronously. To respond synchronuously, canned + * answers have to be provided upfront. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof sinon == "undefined") { + var sinon = {}; +} + +sinon.fakeServer = (function () { + var push = [].push; + function F() {} + + function create(proto) { + F.prototype = proto; + return new F(); + } + + function responseArray(handler) { + var response = handler; + + if (Object.prototype.toString.call(handler) != "[object Array]") { + response = [200, {}, handler]; + } + + if (typeof response[2] != "string") { + throw new TypeError("Fake server response body should be string, but was " + + typeof response[2]); + } + + return response; + } + + var wloc = typeof window !== "undefined" ? window.location : {}; + var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); + + function matchOne(response, reqMethod, reqUrl) { + var rmeth = response.method; + var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); + var url = response.url; + var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); + + return matchMethod && matchUrl; + } + + function match(response, request) { + var requestMethod = this.getHTTPMethod(request); + var requestUrl = request.url; + + if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { + requestUrl = requestUrl.replace(rCurrLoc, ""); + } + + if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { + if (typeof response.response == "function") { + var ru = response.url; + var args = [request].concat(!ru ? [] : requestUrl.match(ru).slice(1)); + return response.response.apply(response, args); + } + + return true; + } + + return false; + } + + function log(response, request) { + var str; + + str = "Request:\n" + sinon.format(request) + "\n\n"; + str += "Response:\n" + sinon.format(response) + "\n\n"; + + sinon.log(str); + } + + return { + create: function () { + var server = create(this); + this.xhr = sinon.useFakeXMLHttpRequest(); + server.requests = []; + + this.xhr.onCreate = function (xhrObj) { + server.addRequest(xhrObj); + }; + + return server; + }, + + addRequest: function addRequest(xhrObj) { + var server = this; + push.call(this.requests, xhrObj); + + xhrObj.onSend = function () { + server.handleRequest(this); + }; + + if (this.autoRespond && !this.responding) { + setTimeout(function () { + server.responding = false; + server.respond(); + }, this.autoRespondAfter || 10); + + this.responding = true; + } + }, + + getHTTPMethod: function getHTTPMethod(request) { + if (this.fakeHTTPMethods && /post/i.test(request.method)) { + var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); + return !!matches ? matches[1] : request.method; + } + + return request.method; + }, + + handleRequest: function handleRequest(xhr) { + if (xhr.async) { + if (!this.queue) { + this.queue = []; + } + + push.call(this.queue, xhr); + } else { + this.processRequest(xhr); + } + }, + + respondWith: function respondWith(method, url, body) { + if (arguments.length == 1 && typeof method != "function") { + this.response = responseArray(method); + return; + } + + if (!this.responses) { this.responses = []; } + + if (arguments.length == 1) { + body = method; + url = method = null; + } + + if (arguments.length == 2) { + body = url; + url = method; + method = null; + } + + push.call(this.responses, { + method: method, + url: url, + response: typeof body == "function" ? body : responseArray(body) + }); + }, + + respond: function respond() { + if (arguments.length > 0) this.respondWith.apply(this, arguments); + var queue = this.queue || []; + var request; + + while(request = queue.shift()) { + this.processRequest(request); + } + }, + + processRequest: function processRequest(request) { + try { + if (request.aborted) { + return; + } + + var response = this.response || [404, {}, ""]; + + if (this.responses) { + for (var i = 0, l = this.responses.length; i < l; i++) { + if (match.call(this, this.responses[i], request)) { + response = this.responses[i].response; + break; + } + } + } + + if (request.readyState != 4) { + log(response, request); + + request.respond(response[0], response[1], response[2]); + } + } catch (e) { + sinon.logError("Fake server request processing", e); + } + }, + + restore: function restore() { + return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); + } + }; +}()); + +if (typeof module == "object" && typeof require == "function") { + module.exports = sinon; +} + +/** + * @depend fake_server.js + * @depend fake_timers.js + */ +/*jslint browser: true, eqeqeq: false, onevar: false*/ +/*global sinon*/ +/** + * Add-on for sinon.fakeServer that automatically handles a fake timer along with + * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery + * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, + * it polls the object for completion with setInterval. Dispite the direct + * motivation, there is nothing jQuery-specific in this file, so it can be used + * in any environment where the ajax implementation depends on setInterval or + * setTimeout. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function () { + function Server() {} + Server.prototype = sinon.fakeServer; + + sinon.fakeServerWithClock = new Server(); + + sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { + if (xhr.async) { + if (typeof setTimeout.clock == "object") { + this.clock = setTimeout.clock; + } else { + this.clock = sinon.useFakeTimers(); + this.resetClock = true; + } + + if (!this.longestTimeout) { + var clockSetTimeout = this.clock.setTimeout; + var clockSetInterval = this.clock.setInterval; + var server = this; + + this.clock.setTimeout = function (fn, timeout) { + server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); + + return clockSetTimeout.apply(this, arguments); + }; + + this.clock.setInterval = function (fn, timeout) { + server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); + + return clockSetInterval.apply(this, arguments); + }; + } + } + + return sinon.fakeServer.addRequest.call(this, xhr); + }; + + sinon.fakeServerWithClock.respond = function respond() { + var returnVal = sinon.fakeServer.respond.apply(this, arguments); + + if (this.clock) { + this.clock.tick(this.longestTimeout || 0); + this.longestTimeout = 0; + + if (this.resetClock) { + this.clock.restore(); + this.resetClock = false; + } + } + + return returnVal; + }; + + sinon.fakeServerWithClock.restore = function restore() { + if (this.clock) { + this.clock.restore(); + } + + return sinon.fakeServer.restore.apply(this, arguments); + }; +}()); + +/** + * @depend ../sinon.js + * @depend collection.js + * @depend util/fake_timers.js + * @depend util/fake_server_with_clock.js + */ +/*jslint eqeqeq: false, onevar: false, plusplus: false*/ +/*global require, module*/ +/** + * Manages fake collections as well as fake utilities such as Sinon's + * timers and fake XHR implementation in one convenient object. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +if (typeof module == "object" && typeof require == "function") { + var sinon = require("../sinon"); + sinon.extend(sinon, require("./util/fake_timers")); +} + +(function () { + var push = [].push; + + function exposeValue(sandbox, config, key, value) { + if (!value) { + return; + } + + if (config.injectInto) { + config.injectInto[key] = value; + } else { + push.call(sandbox.args, value); + } + } + + function prepareSandboxFromConfig(config) { + var sandbox = sinon.create(sinon.sandbox); + + if (config.useFakeServer) { + if (typeof config.useFakeServer == "object") { + sandbox.serverPrototype = config.useFakeServer; + } + + sandbox.useFakeServer(); + } + + if (config.useFakeTimers) { + if (typeof config.useFakeTimers == "object") { + sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); + } else { + sandbox.useFakeTimers(); + } + } + + return sandbox; + } + + sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { + useFakeTimers: function useFakeTimers() { + this.clock = sinon.useFakeTimers.apply(sinon, arguments); + + return this.add(this.clock); + }, + + serverPrototype: sinon.fakeServer, + + useFakeServer: function useFakeServer() { + var proto = this.serverPrototype || sinon.fakeServer; + + if (!proto || !proto.create) { + return null; + } + + this.server = proto.create(); + return this.add(this.server); + }, + + inject: function (obj) { + sinon.collection.inject.call(this, obj); + + if (this.clock) { + obj.clock = this.clock; + } + + if (this.server) { + obj.server = this.server; + obj.requests = this.server.requests; + } + + return obj; + }, + + create: function (config) { + if (!config) { + return sinon.create(sinon.sandbox); + } + + var sandbox = prepareSandboxFromConfig(config); + sandbox.args = sandbox.args || []; + var prop, value, exposed = sandbox.inject({}); + + if (config.properties) { + for (var i = 0, l = config.properties.length; i < l; i++) { + prop = config.properties[i]; + value = exposed[prop] || prop == "sandbox" && sandbox; + exposeValue(sandbox, config, prop, value); + } + } else { + exposeValue(sandbox, config, "sandbox", value); + } + + return sandbox; + } + }); + + sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; + + if (typeof module == "object" && typeof require == "function") { + module.exports = sinon.sandbox; + } +}()); + +/** + * @depend ../sinon.js + * @depend stub.js + * @depend mock.js + * @depend sandbox.js + */ +/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Test function, sandboxes fakes + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function test(callback) { + var type = typeof callback; + + if (type != "function") { + throw new TypeError("sinon.test needs to wrap a test function, got " + type); + } + + return function () { + var config = sinon.getConfig(sinon.config); + config.injectInto = config.injectIntoThis && this || config.injectInto; + var sandbox = sinon.sandbox.create(config); + var exception, result; + var args = Array.prototype.slice.call(arguments).concat(sandbox.args); + + try { + result = callback.apply(this, args); + } catch (e) { + exception = e; + } + + if (typeof exception !== "undefined") { + sandbox.restore(); + throw exception; + } + else { + sandbox.verifyAndRestore(); + } + + return result; + }; + } + + test.config = { + injectIntoThis: true, + injectInto: null, + properties: ["spy", "stub", "mock", "clock", "server", "requests"], + useFakeTimers: true, + useFakeServer: true + }; + + if (commonJSModule) { + module.exports = test; + } else { + sinon.test = test; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend test.js + */ +/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ +/*global module, require, sinon*/ +/** + * Test case, sandboxes all test functions + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon || !Object.prototype.hasOwnProperty) { + return; + } + + function createTest(property, setUp, tearDown) { + return function () { + if (setUp) { + setUp.apply(this, arguments); + } + + var exception, result; + + try { + result = property.apply(this, arguments); + } catch (e) { + exception = e; + } + + if (tearDown) { + tearDown.apply(this, arguments); + } + + if (exception) { + throw exception; + } + + return result; + }; + } + + function testCase(tests, prefix) { + /*jsl:ignore*/ + if (!tests || typeof tests != "object") { + throw new TypeError("sinon.testCase needs an object with test functions"); + } + /*jsl:end*/ + + prefix = prefix || "test"; + var rPrefix = new RegExp("^" + prefix); + var methods = {}, testName, property, method; + var setUp = tests.setUp; + var tearDown = tests.tearDown; + + for (testName in tests) { + if (tests.hasOwnProperty(testName)) { + property = tests[testName]; + + if (/^(setUp|tearDown)$/.test(testName)) { + continue; + } + + if (typeof property == "function" && rPrefix.test(testName)) { + method = property; + + if (setUp || tearDown) { + method = createTest(property, setUp, tearDown); + } + + methods[testName] = sinon.test(method); + } else { + methods[testName] = tests[testName]; + } + } + } + + return methods; + } + + if (commonJSModule) { + module.exports = testCase; + } else { + sinon.testCase = testCase; + } +}(typeof sinon == "object" && sinon || null)); + +/** + * @depend ../sinon.js + * @depend stub.js + */ +/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ +/*global module, require, sinon*/ +/** + * Assertions matching the test spy retrieval interface. + * + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2010-2013 Christian Johansen + */ + +(function (sinon, global) { + var commonJSModule = typeof module == "object" && typeof require == "function"; + var slice = Array.prototype.slice; + var assert; + + if (!sinon && commonJSModule) { + sinon = require("../sinon"); + } + + if (!sinon) { + return; + } + + function verifyIsStub() { + var method; + + for (var i = 0, l = arguments.length; i < l; ++i) { + method = arguments[i]; + + if (!method) { + assert.fail("fake is not a spy"); + } + + if (typeof method != "function") { + assert.fail(method + " is not a function"); + } + + if (typeof method.getCall != "function") { + assert.fail(method + " is not stubbed"); + } + } + } + + function failAssertion(object, msg) { + object = object || global; + var failMethod = object.fail || assert.fail; + failMethod.call(object, msg); + } + + function mirrorPropAsAssertion(name, method, message) { + if (arguments.length == 2) { + message = method; + method = name; + } + + assert[name] = function (fake) { + verifyIsStub(fake); + + var args = slice.call(arguments, 1); + var failed = false; + + if (typeof method == "function") { + failed = !method(fake); + } else { + failed = typeof fake[method] == "function" ? + !fake[method].apply(fake, args) : !fake[method]; + } + + if (failed) { + failAssertion(this, fake.printf.apply(fake, [message].concat(args))); + } else { + assert.pass(name); + } + }; + } + + function exposedName(prefix, prop) { + return !prefix || /^fail/.test(prop) ? prop : + prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); + }; + + assert = { + failException: "AssertError", + + fail: function fail(message) { + var error = new Error(message); + error.name = this.failException || assert.failException; + + throw error; + }, + + pass: function pass(assertion) {}, + + callOrder: function assertCallOrder() { + verifyIsStub.apply(null, arguments); + var expected = "", actual = ""; + + if (!sinon.calledInOrder(arguments)) { + try { + expected = [].join.call(arguments, ", "); + var calls = slice.call(arguments); + var i = calls.length; + while (i) { + if (!calls[--i].called) { + calls.splice(i, 1); + } + } + actual = sinon.orderByFirstCall(calls).join(", "); + } catch (e) { + // If this fails, we'll just fall back to the blank string + } + + failAssertion(this, "expected " + expected + " to be " + + "called in order but were called as " + actual); + } else { + assert.pass("callOrder"); + } + }, + + callCount: function assertCallCount(method, count) { + verifyIsStub(method); + + if (method.callCount != count) { + var msg = "expected %n to be called " + sinon.timesInWords(count) + + " but was called %c%C"; + failAssertion(this, method.printf(msg)); + } else { + assert.pass("callCount"); + } + }, + + expose: function expose(target, options) { + if (!target) { + throw new TypeError("target is null or undefined"); + } + + var o = options || {}; + var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix; + var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail; + + for (var method in this) { + if (method != "export" && (includeFail || !/^(fail)/.test(method))) { + target[exposedName(prefix, method)] = this[method]; + } + } + + return target; + } + }; + + mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called"); + mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; }, + "expected %n to not have been called but was called %c%C"); + mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C"); + mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); + mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); + mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); + mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); + mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new"); + mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new"); + mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C"); + mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %*%C"); + mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); + mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %*%C"); + mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); + mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); + mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C"); + mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C"); + mirrorPropAsAssertion("threw", "%n did not throw exception%C"); + mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C"); + + if (commonJSModule) { + module.exports = assert; + } else { + sinon.assert = assert; + } +}(typeof sinon == "object" && sinon || null, typeof window != "undefined" ? window : (typeof self != "undefined") ? self : global)); + +return sinon;}.call(typeof window != 'undefined' && window || {})); From 5122672333dc0009af90c6e728c7d79031516f61 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:18:22 +0100 Subject: [PATCH 15/74] Fixed date-parsing logic so that it works on oldIE. Issue #323. --- src/view.timeline.js | 10 +++++----- test/view.timeline.test.js | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/view.timeline.js b/src/view.timeline.js index 40aa7563..81b02784 100644 --- a/src/view.timeline.js +++ b/src/view.timeline.js @@ -134,14 +134,14 @@ my.Timeline = Backbone.View.extend({ if (!date) { return null; } - var out = date.trim(); + var out = $.trim(date); out = out.replace(/(\d)th/g, '$1'); out = out.replace(/(\d)st/g, '$1'); - out = out.trim() ? moment(out) : null; - if (out.toDate() == 'Invalid Date') { - return null; - } else { + out = $.trim(out) ? moment(out) : null; + if (out && out.isValid()) { return out.toDate(); + } else { + return null; } }, diff --git a/test/view.timeline.test.js b/test/view.timeline.test.js index 0b6637d8..5e3bf148 100644 --- a/test/view.timeline.test.js +++ b/test/view.timeline.test.js @@ -19,13 +19,13 @@ test('extract dates and timelineJSON', function () { 'headline': '', 'date': [ { - 'startDate': new Date('2012-03-20'), + 'startDate': moment('2012-03-20').toDate(), 'endDate': null, 'headline': '1', 'text': '
Date: 2012-03-20
title: 1
' }, { - 'startDate': new Date('2012-03-25'), + 'startDate': moment('2012-03-25').toDate(), 'endDate': null, 'headline': '2', 'text': '
Date: 2012-03-25
title: 2
' From 1c86c70c3204933e648274a942a712eba55c8269 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:37:11 +0100 Subject: [PATCH 16/74] Use htmlEqual() so that we're more robust against cross-browser serialisation issues. Issue #323. --- test/view.map.test.js | 4 ++-- test/view.slickgrid.test.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/view.map.test.js b/test/view.map.test.js index 8102ec87..598c1a05 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -183,7 +183,7 @@ test('Popup', function () { view.remove(); }); -test('Popup - Custom', function () { +test('Popup - Custom', function (assert) { var dataset = GeoJSONFixture.getDataset(); var view = new recline.View.Map({ model: dataset @@ -202,7 +202,7 @@ test('Popup - Custom', function () { assertPresent(popup); var text = popup.html(); - ok((text.indexOf('

1

y: 2') != -1)) + assert.htmlEqual(text, '

1

y: 2'); view.remove(); }); diff --git a/test/view.slickgrid.test.js b/test/view.slickgrid.test.js index de54e9cb..f4686dde 100644 --- a/test/view.slickgrid.test.js +++ b/test/view.slickgrid.test.js @@ -128,7 +128,7 @@ test('update', function() { view.remove(); }); -test('renderers', function () { +test('renderers', function (assert) { var dataset = Fixture.getDataset(); dataset.fields.get('country').renderer = function(val, field, doc){ @@ -150,7 +150,7 @@ test('renderers', function () { view.grid.init(); equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).text(),'Country: DE'); - equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).html(),'Country: DE'); + assert.htmlEqual($(view.grid.getCellNode(0,view.grid.getColumnIndex('country'))).html(),'Country: DE'); equal($(view.grid.getCellNode(0,view.grid.getColumnIndex('computed'))).text(),'10'); view.remove(); }); From 6883d8a1c487dad87a33b480284eaa326a03d3f9 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:38:54 +0100 Subject: [PATCH 17/74] Fixed invalid HTML in the MapMenu template. Issue #323. --- src/view.map.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/view.map.js b/src/view.map.js index c0979a2a..3adbb96e 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -531,7 +531,6 @@ my.MapMenu = Backbone.View.extend({ Cluster markers \ \ \ - \ \ ', From 56a8b64f812627b7bca715cdbd5372fc86135f3b Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:40:36 +0100 Subject: [PATCH 18/74] GridRow's constructor expected a FieldList object, but it was sometimes getting a plain array. Issue #323. --- src/view.grid.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/view.grid.js b/src/view.grid.js index 394b205d..88ebd213 100644 --- a/src/view.grid.js +++ b/src/view.grid.js @@ -87,7 +87,7 @@ my.Grid = Backbone.View.extend({ var modelData = this.model.toJSON(); modelData.notEmpty = ( this.fields.length > 0 ); // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = _.map(this.fields, function(field) { + modelData.fields = this.fields.map(function(field) { return field.toJSON(); }); // last header width = scroll bar - border (2px) */ @@ -96,9 +96,10 @@ my.Grid = Backbone.View.extend({ }, render: function() { var self = this; - this.fields = this.model.fields.filter(function(field) { + this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) { return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; - }); + })); + this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions var numFields = this.fields.length; // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) @@ -106,7 +107,7 @@ my.Grid = Backbone.View.extend({ var width = parseInt(Math.max(50, fullWidth / numFields), 10); // if columns extend outside viewport then remainder is 0 var remainder = Math.max(fullWidth - numFields * width,0); - _.each(this.fields, function(field, idx) { + this.fields.each(function(field, idx) { // add the remainder to the first field width so we make up full col if (idx === 0) { field.set({width: width+remainder}); From 28052c2717a3741afcc3ae1c2a6bb1da94ef1341 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:44:00 +0100 Subject: [PATCH 19/74] Use underscore's indexOf() and filter() rather than relying on native versions. Issue #323. --- src/view.slickgrid.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index d1f1f206..cb0525e5 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -118,7 +118,7 @@ my.SlickGrid = Backbone.View.extend({ }); // Restrict the visible columns - var visibleColumns = columns.filter(function(column) { + var visibleColumns = _.filter(columns, function(column) { return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1; }); @@ -164,7 +164,7 @@ my.SlickGrid = Backbone.View.extend({ this.getItem = function(index) {return rows[index];}; this.getItemMetadata = function(index) {return {};}; this.getModel = function(index) {return models[index];}; - this.getModelRow = function(m) {return models.indexOf(m);}; + this.getModelRow = function(m) {return _.indexOf(models, m);}; this.updateItem = function(m,i) { rows[i] = toRow(m); models[i] = m; From b2c5c7f0e0385719c27cf149b6ce081a1f54fda1 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Tue, 14 May 2013 18:45:43 +0100 Subject: [PATCH 20/74] Misc test suite fixes for IE8. Fixes #323. --- test/model.test.js | 2 +- test/view.slickgrid.test.js | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/model.test.js b/test/model.test.js index e4ba3617..e3179dac 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -279,7 +279,7 @@ test('_normalizeRecordsAndFields', function () { fields: [{id: 'col1'}, {id: 'col2'}], records: [ {col1: 1, col2: 2}, - {col1: 3, col2: 4}, + {col1: 3, col2: 4} ] }, exp: { diff --git a/test/view.slickgrid.test.js b/test/view.slickgrid.test.js index f4686dde..597461cd 100644 --- a/test/view.slickgrid.test.js +++ b/test/view.slickgrid.test.js @@ -83,7 +83,7 @@ test('editable', function () { $('.fixtures .test-datatable').append(view.el); view.render(); - view.grid.init(); + view.show(); var new_item = {lon: "foo", id: 1, z: 23, date: "12", y: 3, country: 'FR'}; @@ -92,13 +92,15 @@ test('editable', function () { }); // Be sure a cell change triggers a change of the model - e = new Slick.EventData(); - return view.grid.onCellChange.notify({ - row: 1, - cell: 0, - item: new_item, - grid: view.grid - }, e, view.grid); + e = new Slick.EventData(); + view.grid.onCellChange.notify({ + row: 1, + cell: 0, + item: new_item, + grid: view.grid + }, e, view.grid); + + view.remove(); }); test('update', function() { From 9932da7b3724c484e1afcca742b086d075d975d7 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Wed, 15 May 2013 11:32:23 +0100 Subject: [PATCH 21/74] Build current. --- dist/recline.dataset.js | 10 +- dist/recline.js | 391 +++++++++++++++++++++------------------- 2 files changed, 209 insertions(+), 192 deletions(-) diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index 3ca6fd41..bd311509 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -3,9 +3,10 @@ this.recline = this.recline || {}; this.recline.Model = this.recline.Model || {}; (function(my) { + "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ @@ -298,7 +299,7 @@ my.Record = Backbone.Model.extend({ // // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { - val = this.getFieldValueUnrendered(field); + var val = this.getFieldValueUnrendered(field); if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } @@ -588,10 +589,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.Memory = this.recline.Backend.Memory || {}; (function(my) { + "use strict"; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Data Wrapper // @@ -693,7 +695,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, string : function (e) { return e.toString() }, - date : function (e) { return new Date(e).valueOf() }, + date : function (e) { return moment(e).valueOf() }, datetime : function (e) { return new Date(e).valueOf() } }; var keyedFields = {}; diff --git a/dist/recline.js b/dist/recline.js index 9df9cacc..87af5a40 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -4,10 +4,11 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // Note that provision of jQuery is optional (it is **only** needed if you use fetch on a remote file) (function(my) { + "use strict"; my.__type__ = 'csv'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## fetch // @@ -295,6 +296,7 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; (function(my) { + "use strict"; my.__type__ = 'dataproxy'; // URL for the dataproxy my.dataproxy_url = '//jsonpdataproxy.appspot.com'; @@ -304,7 +306,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## load // @@ -370,10 +372,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; (function($, my) { + "use strict"; my.__type__ = 'elasticsearch'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## ElasticSearch Wrapper // @@ -567,8 +570,8 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; fields: fieldData }); }) - .fail(function(arguments) { - dfd.reject(arguments); + .fail(function(args) { + dfd.reject(args); }); return dfd.promise(); }; @@ -655,10 +658,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; (function(my) { + "use strict"; my.__type__ = 'gdocs'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Google spreadsheet backend // @@ -822,10 +826,11 @@ this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.Memory = this.recline.Backend.Memory || {}; (function(my) { + "use strict"; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Data Wrapper // @@ -927,7 +932,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, string : function (e) { return e.toString() }, - date : function (e) { return new Date(e).valueOf() }, + date : function (e) { return moment(e).valueOf() }, datetime : function (e) { return new Date(e).valueOf() } }; var keyedFields = {}; @@ -1123,9 +1128,10 @@ this.recline = this.recline || {}; this.recline.Model = this.recline.Model || {}; (function(my) { + "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(this.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ @@ -1418,7 +1424,7 @@ my.Record = Backbone.Model.extend({ // // NB: if field is undefined a default '' value will be returned getFieldValue: function(field) { - val = this.getFieldValueUnrendered(field); + var val = this.getFieldValueUnrendered(field); if (field && !_.isUndefined(field.renderer)) { val = field.renderer(val, field, this.toJSON()); } @@ -1709,7 +1715,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; // ## Graph view for a Dataset using Flot graphing library. // // Initialization arguments (in a hash in first parameter): @@ -1743,14 +1749,11 @@ my.Flot = Backbone.View.extend({ var self = this; this.graphColors = ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"]; - this.el = $(this.el); _.bindAll(this, 'render', 'redraw', '_toolTip', '_xaxisLabel'); this.needToRedraw = false; - this.model.bind('change', this.render); - this.model.fields.bind('reset', this.render); - this.model.fields.bind('add', this.render); - this.model.records.bind('add', this.redraw); - this.model.records.bind('reset', this.redraw); + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model.fields, 'reset add', this.render); + this.listenTo(this.model.records, 'reset add', this.redraw); var stateData = _.extend({ group: null, // so that at least one series chooser box shows up @@ -1765,29 +1768,34 @@ my.Flot = Backbone.View.extend({ model: this.model, state: this.state.toJSON() }); - this.editor.state.bind('change', function() { + this.listenTo(this.editor.state, 'change', function() { self.state.set(self.editor.state.toJSON()); self.redraw(); }); - this.elSidebar = this.editor.el; + this.elSidebar = this.editor.$el; }, render: function() { var self = this; var tmplData = this.model.toTemplateJSON(); var htmls = Mustache.render(this.template, tmplData); - $(this.el).html(htmls); - this.$graph = this.el.find('.panel.graph'); + this.$el.html(htmls); + this.$graph = this.$el.find('.panel.graph'); this.$graph.on("plothover", this._toolTip); return this; }, + remove: function () { + this.editor.remove(); + Backbone.View.prototype.remove.apply(this, arguments); + }, + redraw: function() { // There are issues generating a Flot graph if either: // * The relevant div that graph attaches to his hidden at the moment of creating the plot -- Flot will complain with // Uncaught Invalid dimensions for plot, width = 0, height = 0 // * There is no data for the plot -- either same error or may have issues later with errors like 'non-existent node-value' - var areWeVisible = !jQuery.expr.filters.hidden(this.el[0]); + var areWeVisible = !jQuery.expr.filters.hidden(this.el); if ((!areWeVisible || this.model.records.length === 0)) { this.needToRedraw = true; return; @@ -2108,10 +2116,8 @@ my.FlotControls = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); - this.model.fields.bind('reset', this.render); - this.model.fields.bind('add', this.render); + this.listenTo(this.model.fields, 'reset add', this.render); this.state = new recline.Model.ObjectState(options.state); this.render(); }, @@ -2120,7 +2126,7 @@ my.FlotControls = Backbone.View.extend({ var self = this; var tmplData = this.model.toTemplateJSON(); var htmls = Mustache.render(this.template, tmplData); - this.el.html(htmls); + this.$el.html(htmls); // set up editor from state if (this.state.get('graphType')) { @@ -2144,7 +2150,7 @@ my.FlotControls = Backbone.View.extend({ // Private: Helper function to select an option from a select list // _selectOption: function(id,value){ - var options = this.el.find(id + ' select > option'); + var options = this.$el.find(id + ' select > option'); if (options) { options.each(function(opt){ if (this.value == value) { @@ -2156,16 +2162,16 @@ my.FlotControls = Backbone.View.extend({ }, onEditorSubmit: function(e) { - var select = this.el.find('.editor-group select'); + var select = this.$el.find('.editor-group select'); var $editor = this; - var $series = this.el.find('.editor-series select'); + var $series = this.$el.find('.editor-series select'); var series = $series.map(function () { return $(this).val(); }); var updatedState = { series: $.makeArray(series), - group: this.el.find('.editor-group select').val(), - graphType: this.el.find('.editor-type select').val() + group: this.$el.find('.editor-group select').val(), + graphType: this.$el.find('.editor-type select').val() }; this.state.set(updatedState); }, @@ -2182,7 +2188,7 @@ my.FlotControls = Backbone.View.extend({ }, this.model.toTemplateJSON()); var htmls = Mustache.render(this.templateSeriesEditor, data); - this.el.find('.editor-series-group').append(htmls); + this.$el.find('.editor-series-group').append(htmls); return this; }, @@ -2213,6 +2219,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## (Data) Grid Dataset View // // Provides a tabular view on a Dataset. @@ -2224,11 +2231,8 @@ my.Grid = Backbone.View.extend({ initialize: function(modelEtc) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render', 'onHorizontalScroll'); - this.model.records.bind('add', this.render); - this.model.records.bind('reset', this.render); - this.model.records.bind('remove', this.render); + this.listenTo(this.model.records, 'add reset remove', this.render); this.tempState = {}; var state = _.extend({ hiddenFields: [] @@ -2268,7 +2272,7 @@ my.Grid = Backbone.View.extend({ onHorizontalScroll: function(e) { var currentScroll = $(e.target).scrollLeft(); - this.el.find('.recline-grid thead tr').scrollLeft(currentScroll); + this.$el.find('.recline-grid thead tr').scrollLeft(currentScroll); }, // ====================================================== @@ -2296,7 +2300,7 @@ my.Grid = Backbone.View.extend({ var modelData = this.model.toJSON(); modelData.notEmpty = ( this.fields.length > 0 ); // TODO: move this sort of thing into a toTemplateJSON method on Dataset? - modelData.fields = _.map(this.fields, function(field) { + modelData.fields = this.fields.map(function(field) { return field.toJSON(); }); // last header width = scroll bar - border (2px) */ @@ -2305,17 +2309,18 @@ my.Grid = Backbone.View.extend({ }, render: function() { var self = this; - this.fields = this.model.fields.filter(function(field) { + this.fields = new recline.Model.FieldList(this.model.fields.filter(function(field) { return _.indexOf(self.state.get('hiddenFields'), field.id) == -1; - }); + })); + this.scrollbarDimensions = this.scrollbarDimensions || this._scrollbarSize(); // skip measurement if already have dimensions var numFields = this.fields.length; // compute field widths (-20 for first menu col + 10px for padding on each col and finally 16px for the scrollbar) - var fullWidth = self.el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; + var fullWidth = self.$el.width() - 20 - 10 * numFields - this.scrollbarDimensions.width; var width = parseInt(Math.max(50, fullWidth / numFields), 10); // if columns extend outside viewport then remainder is 0 var remainder = Math.max(fullWidth - numFields * width,0); - _.each(this.fields, function(field, idx) { + this.fields.each(function(field, idx) { // add the remainder to the first field width so we make up full col if (idx === 0) { field.set({width: width+remainder}); @@ -2324,10 +2329,10 @@ my.Grid = Backbone.View.extend({ } }); var htmls = Mustache.render(this.template, this.toTemplateJSON()); - this.el.html(htmls); + this.$el.html(htmls); this.model.records.forEach(function(doc) { var tr = $(''); - self.el.find('tbody').append(tr); + self.$el.find('tbody').append(tr); var newView = new my.GridRow({ model: doc, el: tr, @@ -2336,12 +2341,12 @@ my.Grid = Backbone.View.extend({ newView.render(); }); // hide extra header col if no scrollbar to avoid unsightly overhang - var $tbody = this.el.find('tbody')[0]; + var $tbody = this.$el.find('tbody')[0]; if ($tbody.scrollHeight <= $tbody.offsetHeight) { - this.el.find('th.last-header').hide(); + this.$el.find('th.last-header').hide(); } - this.el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); - this.el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); + this.$el.find('.recline-grid').toggleClass('no-hidden', (self.state.get('hiddenFields').length === 0)); + this.$el.find('.recline-grid tbody').scroll(this.onHorizontalScroll); return this; }, @@ -2377,8 +2382,7 @@ my.GridRow = Backbone.View.extend({ initialize: function(initData) { _.bindAll(this, 'render'); this._fields = initData.fields; - this.el = $(this.el); - this.model.bind('change', this.render); + this.listenTo(this.model, 'change', this.render); }, template: ' \ @@ -2411,9 +2415,9 @@ my.GridRow = Backbone.View.extend({ }, render: function() { - this.el.attr('data-id', this.model.id); + this.$el.attr('data-id', this.model.id); var html = Mustache.render(this.template, this.toTemplateJSON()); - $(this.el).html(html); + this.$el.html(html); return this; }, @@ -2433,7 +2437,7 @@ my.GridRow = Backbone.View.extend({ ', onEditClick: function(e) { - var editing = this.el.find('.data-table-cell-editor-editor'); + var editing = this.$el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { editing.parents('.data-table-cell-value').html(editing.text()).siblings('.data-table-cell-edit').removeClass("hidden"); } @@ -2479,7 +2483,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; // ## Map view for a Dataset using Leaflet mapping library. // // This view allows to plot gereferenced records on a map. The location @@ -2526,7 +2530,6 @@ my.Map = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this.visible = true; this.mapReady = false; // this will be the Leaflet L.Map object (setup below) @@ -2553,32 +2556,32 @@ my.Map = Backbone.View.extend({ }; // Listen to changes in the fields - this.model.fields.bind('change', function() { + this.listenTo(this.model.fields, 'change', function() { self._setupGeometryField(); self.render(); }); // Listen to changes in the records - this.model.records.bind('add', function(doc){self.redraw('add',doc);}); - this.model.records.bind('change', function(doc){ + this.listenTo(this.model.records, 'add', function(doc){self.redraw('add',doc);}); + this.listenTo(this.model.records, 'change', function(doc){ self.redraw('remove',doc); self.redraw('add',doc); }); - this.model.records.bind('remove', function(doc){self.redraw('remove',doc);}); - this.model.records.bind('reset', function(){self.redraw('reset');}); + this.listenTo(this.model.records, 'remove', function(doc){self.redraw('remove',doc);}); + this.listenTo(this.model.records, 'reset', function(){self.redraw('reset');}); this.menu = new my.MapMenu({ model: this.model, state: this.state.toJSON() }); - this.menu.state.bind('change', function() { + this.listenTo(this.menu.state, 'change', function() { self.state.set(self.menu.state.toJSON()); self.redraw(); }); - this.state.bind('change', function() { + this.listenTo(this.state, 'change', function() { self.redraw(); }); - this.elSidebar = this.menu.el; + this.elSidebar = this.menu.$el; }, // ## Customization Functions @@ -2598,7 +2601,7 @@ my.Map = Backbone.View.extend({ // } infobox: function(record) { var html = ''; - for (key in record.attributes){ + for (var key in record.attributes){ if (!(this.state.get('geomField') && key == this.state.get('geomField'))){ html += '
' + key + ': '+ record.attributes[key] + '
'; } @@ -2647,10 +2650,9 @@ my.Map = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - - htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls); - this.$map = this.el.find('.panel.map'); + var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + this.$el.html(htmls); + this.$map = this.$el.find('.panel.map'); this.redraw(); return this; }, @@ -2801,7 +2803,7 @@ my.Map = Backbone.View.extend({ if (!(docs instanceof Array)) docs = [docs]; _.each(docs,function(doc){ - for (key in self.features._layers){ + for (var key in self.features._layers){ if (self.features._layers[key].feature.properties.cid == doc.cid){ self.features.removeLayer(self.features._layers[key]); } @@ -3008,7 +3010,6 @@ my.MapMenu = Backbone.View.extend({ Cluster markers \ \ \ - \ \ ', @@ -3022,11 +3023,10 @@ my.MapMenu = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); - this.model.fields.bind('change', this.render); + this.listenTo(this.model.fields, 'change', this.render); this.state = new recline.Model.ObjectState(options.state); - this.state.bind('change', this.render); + this.listenTo(this.state, 'change', this.render); this.render(); }, @@ -3035,28 +3035,28 @@ my.MapMenu = Backbone.View.extend({ // Also sets up the editor fields and the map if necessary. render: function() { var self = this; - htmls = Mustache.render(this.template, this.model.toTemplateJSON()); - $(this.el).html(htmls); + var htmls = Mustache.render(this.template, this.model.toTemplateJSON()); + this.$el.html(htmls); if (this._geomReady() && this.model.fields.length){ if (this.state.get('geomField')){ this._selectOption('editor-geom-field',this.state.get('geomField')); - this.el.find('#editor-field-type-geom').attr('checked','checked').change(); + this.$el.find('#editor-field-type-geom').attr('checked','checked').change(); } else{ this._selectOption('editor-lon-field',this.state.get('lonField')); this._selectOption('editor-lat-field',this.state.get('latField')); - this.el.find('#editor-field-type-latlon').attr('checked','checked').change(); + this.$el.find('#editor-field-type-latlon').attr('checked','checked').change(); } } if (this.state.get('autoZoom')) { - this.el.find('#editor-auto-zoom').attr('checked', 'checked'); + this.$el.find('#editor-auto-zoom').attr('checked', 'checked'); } else { - this.el.find('#editor-auto-zoom').removeAttr('checked'); + this.$el.find('#editor-auto-zoom').removeAttr('checked'); } if (this.state.get('cluster')) { - this.el.find('#editor-cluster').attr('checked', 'checked'); + this.$el.find('#editor-cluster').attr('checked', 'checked'); } else { - this.el.find('#editor-cluster').removeAttr('checked'); + this.$el.find('#editor-cluster').removeAttr('checked'); } return this; }, @@ -3075,17 +3075,17 @@ my.MapMenu = Backbone.View.extend({ // onEditorSubmit: function(e){ e.preventDefault(); - if (this.el.find('#editor-field-type-geom').attr('checked')){ + if (this.$el.find('#editor-field-type-geom').attr('checked')){ this.state.set({ - geomField: this.el.find('.editor-geom-field > select > option:selected').val(), + geomField: this.$el.find('.editor-geom-field > select > option:selected').val(), lonField: null, latField: null }); } else { this.state.set({ geomField: null, - lonField: this.el.find('.editor-lon-field > select > option:selected').val(), - latField: this.el.find('.editor-lat-field > select > option:selected').val() + lonField: this.$el.find('.editor-lon-field > select > option:selected').val(), + latField: this.$el.find('.editor-lat-field > select > option:selected').val() }); } return false; @@ -3096,11 +3096,11 @@ my.MapMenu = Backbone.View.extend({ // onFieldTypeChange: function(e){ if (e.target.value == 'geom'){ - this.el.find('.editor-field-type-geom').show(); - this.el.find('.editor-field-type-latlon').hide(); + this.$el.find('.editor-field-type-geom').show(); + this.$el.find('.editor-field-type-latlon').hide(); } else { - this.el.find('.editor-field-type-geom').hide(); - this.el.find('.editor-field-type-latlon').show(); + this.$el.find('.editor-field-type-geom').hide(); + this.$el.find('.editor-field-type-latlon').show(); } }, @@ -3115,7 +3115,7 @@ my.MapMenu = Backbone.View.extend({ // Private: Helper function to select an option from a select list // _selectOption: function(id,value){ - var options = this.el.find('.' + id + ' > select > option'); + var options = this.$el.find('.' + id + ' > select > option'); if (options){ options.each(function(opt){ if (this.value == value) { @@ -3136,6 +3136,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## MultiView // // Manage multiple views together along with query editor etc. Usage: @@ -3260,7 +3261,6 @@ my.MultiView = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this._setupState(options.state); // Hash of 'page' views (i.e. those for whole page) keyed by page name @@ -3330,30 +3330,30 @@ my.MultiView = Backbone.View.extend({ } this._showHideSidebar(); - this.model.bind('query:start', function() { - self.notify({loader: true, persist: true}); - }); - this.model.bind('query:done', function() { - self.clearNotifications(); - self.el.find('.doc-count').text(self.model.recordCount || 'Unknown'); - }); - this.model.bind('query:fail', function(error) { - self.clearNotifications(); - var msg = ''; - if (typeof(error) == 'string') { - msg = error; - } else if (typeof(error) == 'object') { - if (error.title) { - msg = error.title + ': '; - } - if (error.message) { - msg += error.message; - } - } else { - msg = 'There was an error querying the backend'; + this.listenTo(this.model, 'query:start', function() { + self.notify({loader: true, persist: true}); + }); + this.listenTo(this.model, 'query:done', function() { + self.clearNotifications(); + self.$el.find('.doc-count').text(self.model.recordCount || 'Unknown'); + }); + this.listenTo(this.model, 'query:fail', function(error) { + self.clearNotifications(); + var msg = ''; + if (typeof(error) == 'string') { + msg = error; + } else if (typeof(error) == 'object') { + if (error.title) { + msg = error.title + ': '; } - self.notify({message: msg, category: 'error', persist: true}); - }); + if (error.message) { + msg += error.message; + } + } else { + msg = 'There was an error querying the backend'; + } + self.notify({message: msg, category: 'error', persist: true}); + }); // retrieve basic data like fields etc // note this.model and dataset returned are the same @@ -3366,7 +3366,7 @@ my.MultiView = Backbone.View.extend({ }, setReadOnly: function() { - this.el.addClass('recline-read-only'); + this.$el.addClass('recline-read-only'); }, render: function() { @@ -3374,11 +3374,11 @@ my.MultiView = Backbone.View.extend({ tmplData.views = this.pageViews; tmplData.sidebarViews = this.sidebarViews; var template = Mustache.render(this.template, tmplData); - $(this.el).html(template); + this.$el.html(template); // now create and append other views - var $dataViewContainer = this.el.find('.data-view-container'); - var $dataSidebar = this.el.find('.data-view-sidebar'); + var $dataViewContainer = this.$el.find('.data-view-container'); + var $dataSidebar = this.$el.find('.data-view-sidebar'); // the main views _.each(this.pageViews, function(view, pageName) { @@ -3390,25 +3390,37 @@ my.MultiView = Backbone.View.extend({ }); _.each(this.sidebarViews, function(view) { - this['$'+view.id] = view.view.el; + this['$'+view.id] = view.view.$el; $dataSidebar.append(view.view.el); }, this); - var pager = new recline.View.Pager({ + this.pager = new recline.View.Pager({ model: this.model.queryState }); - this.el.find('.recline-results-info').after(pager.el); + this.$el.find('.recline-results-info').after(this.pager.el); - var queryEditor = new recline.View.QueryEditor({ + this.queryEditor = new recline.View.QueryEditor({ model: this.model.queryState }); - this.el.find('.query-editor-here').append(queryEditor.el); + this.$el.find('.query-editor-here').append(this.queryEditor.el); }, + remove: function () { + _.each(this.pageViews, function (view) { + view.view.remove(); + }); + _.each(this.sidebarViews, function (view) { + view.view.remove(); + }); + this.pager.remove(); + this.queryEditor.remove(); + Backbone.View.prototype.remove.apply(this, arguments); + }, + // hide the sidebar if empty _showHideSidebar: function() { - var $dataSidebar = this.el.find('.data-view-sidebar'); + var $dataSidebar = this.$el.find('.data-view-sidebar'); var visibleChildren = $dataSidebar.children().filter(function() { return $(this).css("display") != "none"; }).length; @@ -3421,19 +3433,19 @@ my.MultiView = Backbone.View.extend({ }, updateNav: function(pageName) { - this.el.find('.navigation a').removeClass('active'); - var $el = this.el.find('.navigation a[data-view="' + pageName + '"]'); + this.$el.find('.navigation a').removeClass('active'); + var $el = this.$el.find('.navigation a[data-view="' + pageName + '"]'); $el.addClass('active'); // add/remove sidebars and hide inactive views _.each(this.pageViews, function(view, idx) { if (view.id === pageName) { - view.view.el.show(); + view.view.$el.show(); if (view.view.elSidebar) { view.view.elSidebar.show(); } } else { - view.view.el.hide(); + view.view.$el.hide(); if (view.view.elSidebar) { view.view.elSidebar.hide(); } @@ -3502,7 +3514,7 @@ my.MultiView = Backbone.View.extend({ _bindStateChanges: function() { var self = this; // finally ensure we update our state object when state of sub-object changes so that state is always up to date - this.model.queryState.bind('change', function() { + this.listenTo(this.model.queryState, 'change', function() { self.state.set({query: self.model.queryState.toJSON()}); }); _.each(this.pageViews, function(pageView) { @@ -3510,7 +3522,7 @@ my.MultiView = Backbone.View.extend({ var update = {}; update['view-' + pageView.id] = pageView.view.state.toJSON(); self.state.set(update); - pageView.view.state.bind('change', function() { + self.listenTo(pageView.view.state, 'change', function() { var update = {}; update['view-' + pageView.id] = pageView.view.state.toJSON(); // had problems where change not being triggered for e.g. grid view so let's do it explicitly @@ -3524,7 +3536,7 @@ my.MultiView = Backbone.View.extend({ _bindFlashNotifications: function() { var self = this; _.each(this.pageViews, function(pageView) { - pageView.view.bind('recline:flash', function(flash) { + self.listenTo(pageView.view, 'recline:flash', function(flash) { self.notify(flash); }); }); @@ -3650,7 +3662,7 @@ my.parseQueryString = function(q) { // Parse the query string out of the URL hash my.parseHashQueryString = function() { - q = my.parseHashUrl(window.location.hash).query; + var q = my.parseHashUrl(window.location.hash).query; return my.parseQueryString(q); }; @@ -3690,6 +3702,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## SlickGrid Dataset View // // Provides a tabular view on a Dataset, based on SlickGrid. @@ -3719,13 +3732,10 @@ this.recline.View = this.recline.View || {}; my.SlickGrid = Backbone.View.extend({ initialize: function(modelEtc) { var self = this; - this.el = $(this.el); - this.el.addClass('recline-slickgrid'); - _.bindAll(this, 'render'); - this.model.records.bind('add', this.render); - this.model.records.bind('reset', this.render); - this.model.records.bind('remove', this.render); - this.model.records.bind('change', this.onRecordChanged, this); + this.$el.addClass('recline-slickgrid'); + _.bindAll(this, 'render', 'onRecordChanged'); + this.listenTo(this.model.records, 'add remove reset', this.render); + this.listenTo(this.model.records, 'change', this.onRecordChanged); var state = _.extend({ hiddenColumns: [], @@ -3739,6 +3749,8 @@ my.SlickGrid = Backbone.View.extend({ ); this.state = new recline.Model.ObjectState(state); + + this._slickHandler = new Slick.EventHandler(); }, events: { @@ -3804,7 +3816,7 @@ my.SlickGrid = Backbone.View.extend({ }); // Restrict the visible columns - var visibleColumns = columns.filter(function(column) { + var visibleColumns = _.filter(columns, function(column) { return _.indexOf(self.state.get('hiddenColumns'), column.id) === -1; }); @@ -3850,7 +3862,7 @@ my.SlickGrid = Backbone.View.extend({ this.getItem = function(index) {return rows[index];}; this.getItemMetadata = function(index) {return {};}; this.getModel = function(index) {return models[index];}; - this.getModelRow = function(m) {return models.indexOf(m);}; + this.getModelRow = function(m) {return _.indexOf(models, m);}; this.updateItem = function(m,i) { rows[i] = toRow(m); models[i] = m; @@ -3873,7 +3885,7 @@ my.SlickGrid = Backbone.View.extend({ this.grid.setSortColumn(column, sortAsc); } - this.grid.onSort.subscribe(function(e, args){ + this._slickHandler.subscribe(this.grid.onSort, function(e, args){ var order = (args.sortAsc) ? 'asc':'desc'; var sort = [{ field: args.sortCol.field, @@ -3882,7 +3894,7 @@ my.SlickGrid = Backbone.View.extend({ self.model.query({sort: sort}); }); - this.grid.onColumnsReordered.subscribe(function(e, args){ + this._slickHandler.subscribe(this.grid.onColumnsReordered, function(e, args){ self.state.set({columnsOrder: _.pluck(self.grid.getColumns(),'id')}); }); @@ -3898,7 +3910,7 @@ my.SlickGrid = Backbone.View.extend({ self.state.set({columnsWidth:columnsWidth}); }); - this.grid.onCellChange.subscribe(function (e, args) { + this._slickHandler.subscribe(this.grid.onCellChange, function (e, args) { // We need to change the model associated value // var grid = args.grid; @@ -3921,7 +3933,12 @@ my.SlickGrid = Backbone.View.extend({ } return this; - }, + }, + + remove: function () { + this._slickHandler.unsubscribeAll(); + Backbone.View.prototype.remove.apply(this, arguments); + }, show: function() { // If the div is hidden, SlickGrid will calculate wrongly some @@ -4067,6 +4084,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // turn off unnecessary logging from VMM Timeline if (typeof VMM !== 'undefined') { VMM.debug = false; @@ -4090,13 +4108,12 @@ my.Timeline = Backbone.View.extend({ initialize: function(options) { var self = this; - this.el = $(this.el); this.timeline = new VMM.Timeline(); this._timelineIsInitialized = false; - this.model.fields.bind('reset', function() { + this.listenTo(this.model.fields, 'reset', function() { self._setupTemporalField(); }); - this.model.records.bind('all', function() { + this.listenTo(this.model.records, 'all', function() { self.reloadData(); }); var stateData = _.extend({ @@ -4113,7 +4130,7 @@ my.Timeline = Backbone.View.extend({ render: function() { var tmplData = {}; var htmls = Mustache.render(this.template, tmplData); - this.el.html(htmls); + this.$el.html(htmls); // can only call _initTimeline once view in DOM as Timeline uses $ // internally to look up element if ($(this.elementId).length > 0) { @@ -4129,7 +4146,7 @@ my.Timeline = Backbone.View.extend({ }, _initTimeline: function() { - var $timeline = this.el.find(this.elementId); + var $timeline = this.$el.find(this.elementId); var data = this._timelineJSON(); this.timeline.init(data, this.elementId, this.state.get("timelineJSOptions")); this._timelineIsInitialized = true @@ -4197,14 +4214,14 @@ my.Timeline = Backbone.View.extend({ if (!date) { return null; } - var out = date.trim(); + var out = $.trim(date); out = out.replace(/(\d)th/g, '$1'); out = out.replace(/(\d)st/g, '$1'); - out = out.trim() ? moment(out) : null; - if (out.toDate() == 'Invalid Date') { - return null; - } else { + out = $.trim(out) ? moment(out) : null; + if (out && out.isValid()) { return out.toDate(); + } else { + return null; } }, @@ -4234,6 +4251,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; // ## FacetViewer // @@ -4271,9 +4289,8 @@ my.FacetViewer = Backbone.View.extend({ }, initialize: function(model) { _.bindAll(this, 'render'); - this.el = $(this.el); - this.model.facets.bind('all', this.render); - this.model.fields.bind('all', this.render); + this.listenTo(this.model.facets, 'all', this.render); + this.listenTo(this.model.fields, 'all', this.render); this.render(); }, render: function() { @@ -4290,17 +4307,17 @@ my.FacetViewer = Backbone.View.extend({ return facet; }); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); // are there actually any facets to show? if (this.model.facets.length > 0) { - this.el.show(); + this.$el.show(); } else { - this.el.hide(); + this.$el.hide(); } }, onHide: function(e) { e.preventDefault(); - this.el.hide(); + this.$el.hide(); }, onFacetFilter: function(e) { e.preventDefault(); @@ -4338,7 +4355,8 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { - + "use strict"; + my.Fields = Backbone.View.extend({ className: 'recline-fields-view', template: ' \ @@ -4377,13 +4395,12 @@ my.Fields = Backbone.View.extend({ initialize: function(model) { var self = this; - this.el = $(this.el); _.bindAll(this, 'render'); // TODO: this is quite restrictive in terms of when it is re-run // e.g. a change in type will not trigger a re-run atm. // being more liberal (e.g. binding to all) can lead to being called a lot (e.g. for change:width) - this.model.fields.bind('reset', function(action) { + this.listenTo(this.model.fields, 'reset', function(action) { self.model.fields.each(function(field) { field.facets.unbind('all', self.render); field.facets.bind('all', self.render); @@ -4392,7 +4409,7 @@ my.Fields = Backbone.View.extend({ self.model.getFieldsSummary(); self.render(); }); - this.el.find('.collapse').collapse(); + this.$el.find('.collapse').collapse(); this.render(); }, render: function() { @@ -4406,7 +4423,7 @@ my.Fields = Backbone.View.extend({ tmplData.fields.push(out); }); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); @@ -4417,6 +4434,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.FilterEditor = Backbone.View.extend({ className: 'recline-filter-editor well', @@ -4501,11 +4519,9 @@ my.FilterEditor = Backbone.View.extend({ 'submit form.js-add': 'onAddFilter' }, initialize: function() { - this.el = $(this.el); _.bindAll(this, 'render'); - this.model.fields.bind('all', this.render); - this.model.queryState.bind('change', this.render); - this.model.queryState.bind('change:filters:new-blank', this.render); + this.listenTo(this.model.fields, 'all', this.render); + this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render); this.render(); }, render: function() { @@ -4521,13 +4537,13 @@ my.FilterEditor = Backbone.View.extend({ return Mustache.render(self.filterTemplates[this.type], this); }; var out = Mustache.render(this.template, tmplData); - this.el.html(out); + this.$el.html(out); }, onAddFilterShow: function(e) { e.preventDefault(); var $target = $(e.target); $target.hide(); - this.el.find('form.js-add').show(); + this.$el.find('form.js-add').show(); }, onAddFilter: function(e) { e.preventDefault(); @@ -4587,6 +4603,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.Pager = Backbone.View.extend({ className: 'recline-pager', @@ -4607,14 +4624,13 @@ my.Pager = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'render'); - this.el = $(this.el); - this.model.bind('change', this.render); + this.listenTo(this.model, 'change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); - var newFrom = parseInt(this.el.find('input[name="from"]').val()); - var newSize = parseInt(this.el.find('input[name="to"]').val()) - newFrom; + var newFrom = parseInt(this.$el.find('input[name="from"]').val()); + var newSize = parseInt(this.$el.find('input[name="to"]').val()) - newFrom; newFrom = Math.max(newFrom, 0); newSize = Math.max(newSize, 1); this.model.set({size: newSize, from: newFrom}); @@ -4635,7 +4651,7 @@ my.Pager = Backbone.View.extend({ var tmplData = this.model.toJSON(); tmplData.to = this.model.get('from') + this.model.get('size'); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); @@ -4647,6 +4663,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.QueryEditor = Backbone.View.extend({ className: 'recline-query-editor', @@ -4666,19 +4683,18 @@ my.QueryEditor = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'render'); - this.el = $(this.el); - this.model.bind('change', this.render); + this.listenTo(this.model, 'change', this.render); this.render(); }, onFormSubmit: function(e) { e.preventDefault(); - var query = this.el.find('.text-query input').val(); + var query = this.$el.find('.text-query input').val(); this.model.set({q: query}); }, render: function() { var tmplData = this.model.toJSON(); var templated = Mustache.render(this.template, tmplData); - this.el.html(templated); + this.$el.html(templated); } }); @@ -4690,6 +4706,7 @@ this.recline = this.recline || {}; this.recline.View = this.recline.View || {}; (function($, my) { + "use strict"; my.ValueFilter = Backbone.View.extend({ className: 'recline-filter-editor well', @@ -4736,11 +4753,9 @@ my.ValueFilter = Backbone.View.extend({ 'submit form.js-add': 'onAddFilter' }, initialize: function() { - this.el = $(this.el); _.bindAll(this, 'render'); - this.model.fields.bind('all', this.render); - this.model.queryState.bind('change', this.render); - this.model.queryState.bind('change:filters:new-blank', this.render); + this.listenTo(this.model.fields, 'all', this.render); + this.listenTo(this.model.queryState, 'change change:filters:new-blank', this.render); this.render(); }, render: function() { @@ -4756,7 +4771,7 @@ my.ValueFilter = Backbone.View.extend({ return Mustache.render(self.filterTemplates.term, this); }; var out = Mustache.render(this.template, tmplData); - this.el.html(out); + this.$el.html(out); }, updateFilter: function(input) { var self = this; @@ -4770,7 +4785,7 @@ my.ValueFilter = Backbone.View.extend({ e.preventDefault(); var $target = $(e.target); $target.hide(); - this.el.find('form.js-add').show(); + this.$el.find('form.js-add').show(); }, onAddFilter: function(e) { e.preventDefault(); From 31e25cf51b9f15ae50edd2557a2f0ff1bdbfe09f Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Wed, 15 May 2013 12:40:38 +0100 Subject: [PATCH 22/74] Enable travis-ci --- .travis.yml | 4 ++ test/qunit/runner.js | 139 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 .travis.yml create mode 100644 test/qunit/runner.js diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..227a3756 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "0.10" +script: phantomjs test/qunit/runner.js test/index.html diff --git a/test/qunit/runner.js b/test/qunit/runner.js new file mode 100644 index 00000000..92e7b939 --- /dev/null +++ b/test/qunit/runner.js @@ -0,0 +1,139 @@ +/* + * QtWebKit-powered headless test runner using PhantomJS + * + * PhantomJS binaries: http://phantomjs.org/download.html + * Requires PhantomJS 1.6+ (1.7+ recommended) + * + * Run with: + * phantomjs runner.js [url-of-your-qunit-testsuite] + * + * e.g. + * phantomjs runner.js http://localhost/qunit/test/index.html + */ + +/*global phantom:false, require:false, console:false, window:false, QUnit:false */ + +(function() { + 'use strict'; + + var url, page, timeout, + args = require('system').args; + + // arg[0]: scriptName, args[1...]: arguments + if (args.length < 2 || args.length > 3) { + console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); + phantom.exit(1); + } + + url = args[1]; + page = require('webpage').create(); + if (args[2] !== undefined) { + timeout = parseInt(args[2], 10); + } + + // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onInitialized = function() { + page.evaluate(addLogging); + }; + + page.onCallback = function(message) { + var result, + failed; + + if (message) { + if (message.name === 'QUnit.done') { + result = message.data; + failed = !result || result.failed; + + phantom.exit(failed ? 1 : 0); + } + } + }; + + page.open(url, function(status) { + if (status !== 'success') { + console.error('Unable to access network: ' + status); + phantom.exit(1); + } else { + // Cannot do this verification with the 'DOMContentLoaded' handler because it + // will be too late to attach it if a page does not have any script tags. + var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); + if (qunitMissing) { + console.error('The `QUnit` object is not present on this page.'); + phantom.exit(1); + } + + // Set a timeout on the test running, otherwise tests with async problems will hang forever + if (typeof timeout === 'number') { + setTimeout(function() { + console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); + phantom.exit(1); + }, timeout * 1000); + } + + // Do nothing... the callback mechanism will handle everything! + } + }); + + function addLogging() { + window.document.addEventListener('DOMContentLoaded', function() { + var currentTestAssertions = []; + + QUnit.log(function(details) { + var response; + + // Ignore passing assertions + if (details.result) { + return; + } + + response = details.message || ''; + + if (typeof details.expected !== 'undefined') { + if (response) { + response += ', '; + } + + response += 'expected: ' + details.expected + ', but was: ' + details.actual; + } + + if (details.source) { + response += "\n" + details.source; + } + + currentTestAssertions.push('Failed assertion: ' + response); + }); + + QUnit.testDone(function(result) { + var i, + len, + name = result.module + ': ' + result.name; + + if (result.failed) { + console.log('Test failed: ' + name); + + for (i = 0, len = currentTestAssertions.length; i < len; i++) { + console.log(' ' + currentTestAssertions[i]); + } + } + + currentTestAssertions.length = 0; + }); + + QUnit.done(function(result) { + console.log('Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); + + if (typeof window.callPhantom === 'function') { + window.callPhantom({ + 'name': 'QUnit.done', + 'data': result + }); + } + }); + }, false); + } +})(); From 8f0fe6e5ef0d23acb03199145171bdc88e54c83c Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Wed, 15 May 2013 13:14:12 +0100 Subject: [PATCH 23/74] Added a build status image to the README. Fixes #252. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 177f6b1d..a7fc7367 100755 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/okfn/recline.png)](https://travis-ci.org/okfn/recline) + A simple but powerful library for building data applications in pure Javascript and HTML.

Recline Website - including Overview, Documentation, Demos etc

From be67a2dad4557f4ed66541600af543859b95f4c5 Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Thu, 16 May 2013 11:34:47 +0100 Subject: [PATCH 24/74] Some more jshint cleanup. Issue #340. --- src/backend.elasticsearch.js | 4 ++-- src/backend.gdocs.js | 6 +++--- src/backend.memory.js | 14 +++++++------- src/view.flot.js | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/backend.elasticsearch.js b/src/backend.elasticsearch.js index 5d02965f..3b1343ca 100644 --- a/src/backend.elasticsearch.js +++ b/src/backend.elasticsearch.js @@ -138,7 +138,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; this._convertFilter = function(filter) { var out = {}; - out[filter.type] = {} + out[filter.type] = {}; if (filter.type === 'term') { out.term[filter.field] = filter.term.toLowerCase(); } else if (filter.type === 'geo_distance') { @@ -168,7 +168,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; dataType: this.options.dataType }); return jqxhr; - } + }; }; diff --git a/src/backend.gdocs.js b/src/backend.gdocs.js index 27eaee38..6a7145cd 100644 --- a/src/backend.gdocs.js +++ b/src/backend.gdocs.js @@ -143,14 +143,14 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; if(!!matches) { key = matches[1]; // the gid in url is 0-based and feed url is 1-based - worksheet = parseInt(matches[3]) + 1; + worksheet = parseInt(matches[3], 10) + 1; if (isNaN(worksheet)) { worksheet = 1; } urls = { worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - } + }; } else { // we assume that it's one of the feeds urls @@ -160,7 +160,7 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; urls = { worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - } + }; } return urls; diff --git a/src/backend.memory.js b/src/backend.memory.js index 897d1baf..18c5ca13 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -108,9 +108,9 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; integer: function (e) { return parseFloat(e, 10); }, 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString() }, - date : function (e) { return moment(e).valueOf() }, - datetime : function (e) { return new Date(e).valueOf() } + string : function (e) { return e.toString(); }, + date : function (e) { return moment(e).valueOf(); }, + datetime : function (e) { return new Date(e).valueOf(); } }; var keyedFields = {}; _.each(self.fields, function(field) { @@ -141,8 +141,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var startnull = (filter.start == null || filter.start === ''); - var stopnull = (filter.stop == null || filter.stop === ''); + var startnull = (filter.start === null || filter.start === ''); + var stopnull = (filter.stop === null || filter.stop === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); var start = parse(filter.start); @@ -166,8 +166,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; if (queryObj.q) { var terms = queryObj.q.split(' '); var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase());; - }); + return new RegExp(term.toLowerCase()); + }); results = _.filter(results, function(rawdoc) { var matches = true; _.each(patterns, function(pattern) { diff --git a/src/view.flot.js b/src/view.flot.js index ff1862d9..7bebbf93 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -215,7 +215,7 @@ my.Flot = Backbone.View.extend({ var numTicks = Math.min(this.model.records.length, 15); var increment = this.model.records.length / numTicks; var ticks = []; - for (i=0; i Date: Thu, 16 May 2013 11:38:49 +0100 Subject: [PATCH 25/74] Build current --- dist/recline.dataset.js | 14 +++++++------- dist/recline.js | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index bd311509..4dc804c8 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -694,9 +694,9 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; integer: function (e) { return parseFloat(e, 10); }, 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString() }, - date : function (e) { return moment(e).valueOf() }, - datetime : function (e) { return new Date(e).valueOf() } + string : function (e) { return e.toString(); }, + date : function (e) { return moment(e).valueOf(); }, + datetime : function (e) { return new Date(e).valueOf(); } }; var keyedFields = {}; _.each(self.fields, function(field) { @@ -727,8 +727,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var startnull = (filter.start == null || filter.start === ''); - var stopnull = (filter.stop == null || filter.stop === ''); + var startnull = (filter.start === null || filter.start === ''); + var stopnull = (filter.stop === null || filter.stop === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); var start = parse(filter.start); @@ -752,8 +752,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; if (queryObj.q) { var terms = queryObj.q.split(' '); var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase());; - }); + return new RegExp(term.toLowerCase()); + }); results = _.filter(results, function(rawdoc) { var matches = true; _.each(patterns, function(pattern) { diff --git a/dist/recline.js b/dist/recline.js index 87af5a40..81bf7e8d 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -507,7 +507,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; this._convertFilter = function(filter) { var out = {}; - out[filter.type] = {} + out[filter.type] = {}; if (filter.type === 'term') { out.term[filter.field] = filter.term.toLowerCase(); } else if (filter.type === 'geo_distance') { @@ -537,7 +537,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; dataType: this.options.dataType }); return jqxhr; - } + }; }; @@ -798,14 +798,14 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; if(!!matches) { key = matches[1]; // the gid in url is 0-based and feed url is 1-based - worksheet = parseInt(matches[3]) + 1; + worksheet = parseInt(matches[3], 10) + 1; if (isNaN(worksheet)) { worksheet = 1; } urls = { worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - } + }; } else { // we assume that it's one of the feeds urls @@ -815,7 +815,7 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; urls = { worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - } + }; } return urls; @@ -931,9 +931,9 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; integer: function (e) { return parseFloat(e, 10); }, 'float': function (e) { return parseFloat(e, 10); }, number: function (e) { return parseFloat(e, 10); }, - string : function (e) { return e.toString() }, - date : function (e) { return moment(e).valueOf() }, - datetime : function (e) { return new Date(e).valueOf() } + string : function (e) { return e.toString(); }, + date : function (e) { return moment(e).valueOf(); }, + datetime : function (e) { return new Date(e).valueOf(); } }; var keyedFields = {}; _.each(self.fields, function(field) { @@ -964,8 +964,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var startnull = (filter.start == null || filter.start === ''); - var stopnull = (filter.stop == null || filter.stop === ''); + var startnull = (filter.start === null || filter.start === ''); + var stopnull = (filter.stop === null || filter.stop === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); var start = parse(filter.start); @@ -989,8 +989,8 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; if (queryObj.q) { var terms = queryObj.q.split(' '); var patterns=_.map(terms, function(term) { - return new RegExp(term.toLowerCase());; - }); + return new RegExp(term.toLowerCase()); + }); results = _.filter(results, function(rawdoc) { var matches = true; _.each(patterns, function(pattern) { @@ -1926,7 +1926,7 @@ my.Flot = Backbone.View.extend({ var numTicks = Math.min(this.model.records.length, 15); var increment = this.model.records.length / numTicks; var ticks = []; - for (i=0; i Date: Thu, 16 May 2013 20:48:00 +0100 Subject: [PATCH 26/74] Fixes a bug in querystate handling. Fixes okfn/dataexplorer#120. --- src/model.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/model.js b/src/model.js index b6a9461a..99494134 100644 --- a/src/model.js +++ b/src/model.js @@ -16,6 +16,7 @@ my.Dataset = Backbone.Model.extend({ // ### initialize initialize: function() { + var self = this; _.bindAll(this, 'query'); this.backend = null; if (this.get('backend')) { @@ -35,8 +36,9 @@ my.Dataset = Backbone.Model.extend({ this.facets = new my.FacetList(); this.recordCount = null; this.queryState = new my.Query(); - this.queryState.bind('change', this.query); - this.queryState.bind('facet:add', this.query); + this.queryState.bind('change facet:add', function () { + self.query(); // We want to call query() without any arguments. + }); // store is what we query and save against // store will either be the backend or be a memory store if Backend fetch // tells us to use memory store From 9aa2b95866b8d141f4fc74e0e6ac71f81bf8112a Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Fri, 17 May 2013 16:11:25 +0100 Subject: [PATCH 27/74] Expose the ability to skip initial rows in CSV parser. --- src/backend.csv.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend.csv.js b/src/backend.csv.js index e00aba78..c043ab3b 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -129,7 +129,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // If we are at a EOF or EOR if (inQuote === false && (cur === delimiter || cur === "\n")) { - field = processField(field); + field = processField(field); // Add the current field to the current row row.push(field); // If this is EOR append row to output and flush row @@ -169,6 +169,9 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; row.push(field); out.push(row); + // Expose the ability to discard initial rows + if (options.skipInitialRows) out = out.slice(options.skipInitialRows); + return out; }; From f6a6a41c0742a7c1f712f3f7c6629b7c13d6b83f Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Fri, 17 May 2013 18:27:50 +0100 Subject: [PATCH 28/74] Rewrote Deferred selection code to not use "window". This means it can be used in a web worker. --- src/backend.csv.js | 2 +- src/backend.dataproxy.js | 2 +- src/backend.elasticsearch.js | 2 +- src/backend.gdocs.js | 2 +- src/backend.memory.js | 2 +- src/model.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/backend.csv.js b/src/backend.csv.js index c043ab3b..28d58c2b 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -8,7 +8,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; my.__type__ = 'csv'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## fetch // diff --git a/src/backend.dataproxy.js b/src/backend.dataproxy.js index ae05bf2f..92b5ae7c 100644 --- a/src/backend.dataproxy.js +++ b/src/backend.dataproxy.js @@ -13,7 +13,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## load // diff --git a/src/backend.elasticsearch.js b/src/backend.elasticsearch.js index 3b1343ca..7c32c9ad 100644 --- a/src/backend.elasticsearch.js +++ b/src/backend.elasticsearch.js @@ -7,7 +7,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; my.__type__ = 'elasticsearch'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## ElasticSearch Wrapper // diff --git a/src/backend.gdocs.js b/src/backend.gdocs.js index 6a7145cd..16976884 100644 --- a/src/backend.gdocs.js +++ b/src/backend.gdocs.js @@ -7,7 +7,7 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; my.__type__ = 'gdocs'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Google spreadsheet backend // diff --git a/src/backend.memory.js b/src/backend.memory.js index 18c5ca13..0e6094cc 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -7,7 +7,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Data Wrapper // diff --git a/src/model.js b/src/model.js index 99494134..40f92532 100644 --- a/src/model.js +++ b/src/model.js @@ -6,7 +6,7 @@ this.recline.Model = this.recline.Model || {}; "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ From 8daba7e4e41eae35a1d652dcacf96b1a4b4318bb Mon Sep 17 00:00:00 2001 From: Dan Wilson Date: Fri, 17 May 2013 18:30:48 +0100 Subject: [PATCH 29/74] Build. --- dist/recline.dataset.js | 10 ++++++---- dist/recline.js | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index 4dc804c8..2f9946d6 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -6,7 +6,7 @@ this.recline.Model = this.recline.Model || {}; "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ @@ -16,6 +16,7 @@ my.Dataset = Backbone.Model.extend({ // ### initialize initialize: function() { + var self = this; _.bindAll(this, 'query'); this.backend = null; if (this.get('backend')) { @@ -35,8 +36,9 @@ my.Dataset = Backbone.Model.extend({ this.facets = new my.FacetList(); this.recordCount = null; this.queryState = new my.Query(); - this.queryState.bind('change', this.query); - this.queryState.bind('facet:add', this.query); + this.queryState.bind('change facet:add', function () { + self.query(); // We want to call query() without any arguments. + }); // store is what we query and save against // store will either be the backend or be a memory store if Backend fetch // tells us to use memory store @@ -593,7 +595,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Data Wrapper // diff --git a/dist/recline.js b/dist/recline.js index 81bf7e8d..03a5f7f1 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -8,7 +8,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; my.__type__ = 'csv'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## fetch // @@ -129,7 +129,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // If we are at a EOF or EOR if (inQuote === false && (cur === delimiter || cur === "\n")) { - field = processField(field); + field = processField(field); // Add the current field to the current row row.push(field); // If this is EOR append row to output and flush row @@ -169,6 +169,9 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; row.push(field); out.push(row); + // Expose the ability to discard initial rows + if (options.skipInitialRows) out = out.slice(options.skipInitialRows); + return out; }; @@ -306,7 +309,7 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## load // @@ -376,7 +379,7 @@ this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; my.__type__ = 'elasticsearch'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## ElasticSearch Wrapper // @@ -662,7 +665,7 @@ this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; my.__type__ = 'gdocs'; // use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Google spreadsheet backend // @@ -830,7 +833,7 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; my.__type__ = 'memory'; // private data - use either jQuery or Underscore Deferred depending on what is available - var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; + var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Data Wrapper // @@ -1131,7 +1134,7 @@ this.recline.Model = this.recline.Model || {}; "use strict"; // use either jQuery or Underscore Deferred depending on what is available -var Deferred = _.isUndefined(window.jQuery) ? _.Deferred : jQuery.Deferred; +var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; // ## Dataset my.Dataset = Backbone.Model.extend({ @@ -1141,6 +1144,7 @@ my.Dataset = Backbone.Model.extend({ // ### initialize initialize: function() { + var self = this; _.bindAll(this, 'query'); this.backend = null; if (this.get('backend')) { @@ -1160,8 +1164,9 @@ my.Dataset = Backbone.Model.extend({ this.facets = new my.FacetList(); this.recordCount = null; this.queryState = new my.Query(); - this.queryState.bind('change', this.query); - this.queryState.bind('facet:add', this.query); + this.queryState.bind('change facet:add', function () { + self.query(); // We want to call query() without any arguments. + }); // store is what we query and save against // store will either be the backend or be a memory store if Backend fetch // tells us to use memory store From eb682d673fbd5a354d52a7508f4748e9d29cc345 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 18:38:52 +0100 Subject: [PATCH 30/74] [#359,view/flot][m]: much better time series support by using flot time plugin - fixes #359. * Note you still need to set the type of the field to time for this to work properly (i.e. we do not attempt to guess a column is dates as that is very error-prone). --- _includes/recline-deps.html | 1 + docs/tutorial-views.markdown | 1 + src/view.flot.js | 78 +-- test/index.html | 3 + test/view.flot.test.js | 2 + vendor/flot/jquery.flot.js | 1111 +++++++++++++++++++++---------- vendor/flot/jquery.flot.time.js | 431 ++++++++++++ 7 files changed, 1232 insertions(+), 395 deletions(-) create mode 100644 vendor/flot/jquery.flot.time.js diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index f4969418..cd46ce55 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -29,6 +29,7 @@ + diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown index 550f351e..0a4a40be 100644 --- a/docs/tutorial-views.markdown +++ b/docs/tutorial-views.markdown @@ -130,6 +130,7 @@ library and the Recline Flot Graph view: + {% endhighlight %} diff --git a/src/view.flot.js b/src/view.flot.js index 7bebbf93..5059156e 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -155,25 +155,17 @@ my.Flot = Backbone.View.extend({ }, _xaxisLabel: function (x) { - var xfield = this.model.fields.get(this.state.attributes.group); - - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - - if (this.xvaluesAreIndex) { + if (this._groupFieldIsDateTime()) { + // oddly x comes through as milliseconds *string* (rather than int + // or float) so we have to reparse + x = new Date(parseFloat(x)).toLocaleDateString(); + } else if (this.xvaluesAreIndex) { x = parseInt(x, 10); // HACK: deal with bar graph style cases where x-axis items were strings // In this case x at this point is the index of the item in the list of // records not its actual x-axis value x = this.model.records.models[x].get(this.state.attributes.group); } - if (isDateTime) { - x = new Date(x).toLocaleDateString(); - } - // } else if (isDateTime) { - // x = new Date(parseInt(x, 10)).toLocaleDateString(); - // } return x; }, @@ -188,25 +180,26 @@ my.Flot = Backbone.View.extend({ // @param numPoints the number of points that will be plotted getGraphOptions: function(typeId, numPoints) { var self = this; - - var tickFormatter = function (x) { - // convert x to a string and make sure that it is not too long or the - // tick labels will overlap - // TODO: find a more accurate way of calculating the size of tick labels - var label = self._xaxisLabel(x) || ""; - - if (typeof label !== 'string') { - label = label.toString(); - } - if (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - + var groupFieldIsDateTime = self._groupFieldIsDateTime(); var xaxis = {}; - xaxis.tickFormatter = tickFormatter; + + if (!groupFieldIsDateTime) { + xaxis.tickFormatter = function (x) { + // convert x to a string and make sure that it is not too long or the + // tick labels will overlap + // TODO: find a more accurate way of calculating the size of tick labels + var label = self._xaxisLabel(x) || ""; + + if (typeof label !== 'string') { + label = label.toString(); + } + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; + } + + return label; + }; + } // for labels case we only want ticks at the label intervals // HACK: however we also get this case with Date fields. In that case we @@ -219,6 +212,8 @@ my.Flot = Backbone.View.extend({ ticks.push(parseInt(i*increment, 10)); } xaxis.ticks = ticks; + } else if (groupFieldIsDateTime) { + xaxis.mode = 'time'; } var yaxis = {}; @@ -300,24 +295,31 @@ my.Flot = Backbone.View.extend({ } }, + _groupFieldIsDateTime: function() { + var xfield = this.model.fields.get(this.state.attributes.group); + var xtype = xfield.get('type'); + var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + return isDateTime; + }, + createSeries: function() { var self = this; self.xvaluesAreIndex = false; var series = []; + var xfield = self.model.fields.get(self.state.attributes.group); + var isDateTime = self._groupFieldIsDateTime(); + _.each(this.state.attributes.series, function(field) { var points = []; var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { - var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - if (isDateTime) { - self.xvaluesAreIndex = true; - x = index; + var _date = moment(x); + if (_date.isValid()) { + x = _date.toDate().getTime(); + } } else if (typeof x === 'string') { x = parseFloat(x); if (isNaN(x)) { // assume this is a string label diff --git a/test/index.html b/test/index.html index f51ccd39..9cf784c2 100644 --- a/test/index.html +++ b/test/index.html @@ -5,6 +5,7 @@ Qunit Tests + @@ -14,6 +15,8 @@ + + diff --git a/test/view.flot.test.js b/test/view.flot.test.js index 005250cd..fb458181 100644 --- a/test/view.flot.test.js +++ b/test/view.flot.test.js @@ -48,7 +48,9 @@ test('dates in graph view', function () { 'series': ['y', 'z'] } }); + view.render(); $('.fixtures').append(view.el); + view.redraw(); view.remove(); }); diff --git a/vendor/flot/jquery.flot.js b/vendor/flot/jquery.flot.js index 8cfa6113..aa7e362a 100644 --- a/vendor/flot/jquery.flot.js +++ b/vendor/flot/jquery.flot.js @@ -1,8 +1,9 @@ -/*! Javascript plotting library for jQuery, version 0.8 alpha. - * - * Released under the MIT license by IOLA, December 2007. - * - */ +/* Javascript plotting library for jQuery, version 0.8.1. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +*/ // first an inline dependency, jquery.colorhelpers.js, we inline it here // for convenience @@ -32,6 +33,462 @@ // the actual Flot code (function($) { + + // Cache the prototype hasOwnProperty for faster access + + var hasOwnProperty = Object.prototype.hasOwnProperty; + + /////////////////////////////////////////////////////////////////////////// + // The Canvas object is a wrapper around an HTML5 tag. + // + // @constructor + // @param {string} cls List of classes to apply to the canvas. + // @param {element} container Element onto which to append the canvas. + // + // Requiring a container is a little iffy, but unfortunately canvas + // operations don't work unless the canvas is attached to the DOM. + + function Canvas(cls, container) { + + var element = container.children("." + cls)[0]; + + if (element == null) { + + element = document.createElement("canvas"); + element.className = cls; + + $(element).css({ direction: "ltr", position: "absolute", left: 0, top: 0 }) + .appendTo(container); + + // If HTML5 Canvas isn't available, fall back to [Ex|Flash]canvas + + if (!element.getContext) { + if (window.G_vmlCanvasManager) { + element = window.G_vmlCanvasManager.initElement(element); + } else { + throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); + } + } + } + + this.element = element; + + var context = this.context = element.getContext("2d"); + + // Determine the screen's ratio of physical to device-independent + // pixels. This is the ratio between the canvas width that the browser + // advertises and the number of pixels actually present in that space. + + // The iPhone 4, for example, has a device-independent width of 320px, + // but its screen is actually 640px wide. It therefore has a pixel + // ratio of 2, while most normal devices have a ratio of 1. + + var devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = + context.webkitBackingStorePixelRatio || + context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || + context.oBackingStorePixelRatio || + context.backingStorePixelRatio || 1; + + this.pixelRatio = devicePixelRatio / backingStoreRatio; + + // Size the canvas to match the internal dimensions of its container + + this.resize(container.width(), container.height()); + + // Collection of HTML div layers for text overlaid onto the canvas + + this.textContainer = null; + this.text = {}; + + // Cache of text fragments and metrics, so we can avoid expensively + // re-calculating them when the plot is re-rendered in a loop. + + this._textCache = {}; + } + + // Resizes the canvas to the given dimensions. + // + // @param {number} width New width of the canvas, in pixels. + // @param {number} width New height of the canvas, in pixels. + + Canvas.prototype.resize = function(width, height) { + + if (width <= 0 || height <= 0) { + throw new Error("Invalid dimensions for plot, width = " + width + ", height = " + height); + } + + var element = this.element, + context = this.context, + pixelRatio = this.pixelRatio; + + // Resize the canvas, increasing its density based on the display's + // pixel ratio; basically giving it more pixels without increasing the + // size of its element, to take advantage of the fact that retina + // displays have that many more pixels in the same advertised space. + + // Resizing should reset the state (excanvas seems to be buggy though) + + if (this.width != width) { + element.width = width * pixelRatio; + element.style.width = width + "px"; + this.width = width; + } + + if (this.height != height) { + element.height = height * pixelRatio; + element.style.height = height + "px"; + this.height = height; + } + + // Save the context, so we can reset in case we get replotted. The + // restore ensure that we're really back at the initial state, and + // should be safe even if we haven't saved the initial state yet. + + context.restore(); + context.save(); + + // Scale the coordinate space to match the display density; so even though we + // may have twice as many pixels, we still want lines and other drawing to + // appear at the same size; the extra pixels will just make them crisper. + + context.scale(pixelRatio, pixelRatio); + }; + + // Clears the entire canvas area, not including any overlaid HTML text + + Canvas.prototype.clear = function() { + this.context.clearRect(0, 0, this.width, this.height); + }; + + // Finishes rendering the canvas, including managing the text overlay. + + Canvas.prototype.render = function() { + + var cache = this._textCache; + + // For each text layer, add elements marked as active that haven't + // already been rendered, and remove those that are no longer active. + + for (var layerKey in cache) { + if (hasOwnProperty.call(cache, layerKey)) { + + var layer = this.getTextLayer(layerKey), + layerCache = cache[layerKey]; + + layer.hide(); + + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + + var positions = styleCache[key].positions; + + for (var i = 0, position; position = positions[i]; i++) { + if (position.active) { + if (!position.rendered) { + layer.append(position.element); + position.rendered = true; + } + } else { + positions.splice(i--, 1); + if (position.rendered) { + position.element.detach(); + } + } + } + + if (positions.length == 0) { + delete styleCache[key]; + } + } + } + } + } + + layer.show(); + } + } + }; + + // Creates (if necessary) and returns the text overlay container. + // + // @param {string} classes String of space-separated CSS classes used to + // uniquely identify the text layer. + // @return {object} The jQuery-wrapped text-layer div. + + Canvas.prototype.getTextLayer = function(classes) { + + var layer = this.text[classes]; + + // Create the text layer if it doesn't exist + + if (layer == null) { + + // Create the text layer container, if it doesn't exist + + if (this.textContainer == null) { + this.textContainer = $("
") + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0, + 'font-size': "smaller", + color: "#545454" + }) + .insertAfter(this.element); + } + + layer = this.text[classes] = $("
") + .addClass(classes) + .css({ + position: "absolute", + top: 0, + left: 0, + bottom: 0, + right: 0 + }) + .appendTo(this.textContainer); + } + + return layer; + }; + + // Creates (if necessary) and returns a text info object. + // + // The object looks like this: + // + // { + // width: Width of the text's wrapper div. + // height: Height of the text's wrapper div. + // element: The jQuery-wrapped HTML div containing the text. + // positions: Array of positions at which this text is drawn. + // } + // + // The positions array contains objects that look like this: + // + // { + // active: Flag indicating whether the text should be visible. + // rendered: Flag indicating whether the text is currently visible. + // element: The jQuery-wrapped HTML div containing the text. + // x: X coordinate at which to draw the text. + // y: Y coordinate at which to draw the text. + // } + // + // Each position after the first receives a clone of the original element. + // + // The idea is that that the width, height, and general 'identity' of the + // text is constant no matter where it is placed; the placements are a + // secondary property. + // + // Canvas maintains a cache of recently-used text info objects; getTextInfo + // either returns the cached element or creates a new entry. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {string} text Text string to retrieve info for. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @return {object} a text info object. + + Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) { + + var textStyle, layerCache, styleCache, info; + + // Cast the value to a string, in case we were given a number or such + + text = "" + text; + + // If the font is a font-spec object, generate a CSS font definition + + if (typeof font === "object") { + textStyle = font.style + " " + font.variant + " " + font.weight + " " + font.size + "px/" + font.lineHeight + "px " + font.family; + } else { + textStyle = font; + } + + // Retrieve (or create) the cache for the text's layer and styles + + layerCache = this._textCache[layer]; + + if (layerCache == null) { + layerCache = this._textCache[layer] = {}; + } + + styleCache = layerCache[textStyle]; + + if (styleCache == null) { + styleCache = layerCache[textStyle] = {}; + } + + info = styleCache[text]; + + // If we can't find a matching element in our cache, create a new one + + if (info == null) { + + var element = $("
").html(text) + .css({ + position: "absolute", + 'max-width': width, + top: -9999 + }) + .appendTo(this.getTextLayer(layer)); + + if (typeof font === "object") { + element.css({ + font: textStyle, + color: font.color + }); + } else if (typeof font === "string") { + element.addClass(font); + } + + info = styleCache[text] = { + width: element.outerWidth(true), + height: element.outerHeight(true), + element: element, + positions: [] + }; + + element.detach(); + } + + return info; + }; + + // Adds a text string to the canvas text overlay. + // + // The text isn't drawn immediately; it is marked as rendering, which will + // result in its addition to the canvas on the next render pass. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number} x X coordinate at which to draw the text. + // @param {number} y Y coordinate at which to draw the text. + // @param {string} text Text string to draw. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which to rotate the text, in degrees. + // Angle is currently unused, it will be implemented in the future. + // @param {number=} width Maximum width of the text before it wraps. + // @param {string=} halign Horizontal alignment of the text; either "left", + // "center" or "right". + // @param {string=} valign Vertical alignment of the text; either "top", + // "middle" or "bottom". + + Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign) { + + var info = this.getTextInfo(layer, text, font, angle, width), + positions = info.positions; + + // Tweak the div's position to match the text's alignment + + if (halign == "center") { + x -= info.width / 2; + } else if (halign == "right") { + x -= info.width; + } + + if (valign == "middle") { + y -= info.height / 2; + } else if (valign == "bottom") { + y -= info.height; + } + + // Determine whether this text already exists at this position. + // If so, mark it for inclusion in the next render pass. + + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = true; + return; + } + } + + // If the text doesn't exist at this position, create a new entry + + // For the very first position we'll re-use the original element, + // while for subsequent ones we'll clone it. + + position = { + active: true, + rendered: false, + element: positions.length ? info.element.clone() : info.element, + x: x, + y: y + } + + positions.push(position); + + // Move the element to its final position within the container + + position.element.css({ + top: Math.round(y), + left: Math.round(x), + 'text-align': halign // In case the text wraps + }); + }; + + // Removes one or more text strings from the canvas text overlay. + // + // If no parameters are given, all text within the layer is removed. + // + // Note that the text is not immediately removed; it is simply marked as + // inactive, which will result in its removal on the next render pass. + // This avoids the performance penalty for 'clear and redraw' behavior, + // where we potentially get rid of all text on a layer, but will likely + // add back most or all of it later, as when redrawing axes, for example. + // + // @param {string} layer A string of space-separated CSS classes uniquely + // identifying the layer containing this text. + // @param {number=} x X coordinate of the text. + // @param {number=} y Y coordinate of the text. + // @param {string=} text Text string to remove. + // @param {(string|object)=} font Either a string of space-separated CSS + // classes or a font-spec object, defining the text's font and style. + // @param {number=} angle Angle at which the text is rotated, in degrees. + // Angle is currently unused, it will be implemented in the future. + + Canvas.prototype.removeText = function(layer, x, y, text, font, angle) { + if (text == null) { + var layerCache = this._textCache[layer]; + if (layerCache != null) { + for (var styleKey in layerCache) { + if (hasOwnProperty.call(layerCache, styleKey)) { + var styleCache = layerCache[styleKey]; + for (var key in styleCache) { + if (hasOwnProperty.call(styleCache, key)) { + var positions = styleCache[key].positions; + for (var i = 0, position; position = positions[i]; i++) { + position.active = false; + } + } + } + } + } + } + } else { + var positions = this.getTextInfo(layer, text, font, angle).positions; + for (var i = 0, position; position = positions[i]; i++) { + if (position.x == x && position.y == y) { + position.active = false; + } + } + } + }; + + /////////////////////////////////////////////////////////////////////////// + // The top-level container for the entire plot. + function Plot(placeholder, data_, options_, plugins) { // data is on the form: // [ series1, series2 ... ] @@ -58,8 +515,7 @@ show: null, // null = auto-detect, true = always, false = never position: "bottom", // or "top" mode: null, // null or "time" - timezone: null, // "browser" for local to the client or timezone for timezone-js - font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } + font: null, // null (derived from CSS in placeholder) or object like { size: 11, lineHeight: 13, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" } color: null, // base color, labels, ticks tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)" transform: null, // null or f: number -> number to transform axis @@ -74,14 +530,9 @@ reserveSpace: null, // whether to reserve space even if axis isn't shown tickLength: null, // size in pixels of ticks, or "full" for whole line alignTicksWithAxis: null, // axis number or null for no sync - - // mode specific options tickDecimals: null, // no. of decimals, null means auto tickSize: null, // number or [number, "unit"] - minTickSize: null, // number or [number, "unit"] - monthNames: null, // list of names of months - timeformat: null, // format string to use - twelveHourClock: false // 12 or 24 time in time mode + minTickSize: null // number or [number, "unit"] }, yaxis: { autoscaleMargin: 0.02, @@ -105,6 +556,8 @@ fill: false, fillColor: null, steps: false + // Omit 'zero', so we can later default its value to + // match that of the 'fill' option. }, bars: { show: false, @@ -113,7 +566,8 @@ fill: true, fillColor: null, align: "left", // "left", "right", or "center" - horizontal: false + horizontal: false, + zero: true }, shadowSize: 3, highlightColor: null @@ -144,13 +598,12 @@ }, hooks: {} }, - canvas = null, // the canvas for the plot itself + surface = null, // the canvas for the plot itself overlay = null, // canvas for interactive stuff on top of plot eventHolder = null, // jQuery object that events should be bound to ctx = null, octx = null, xaxes = [], yaxes = [], plotOffset = { left: 0, right: 0, top: 0, bottom: 0}, - canvasWidth = 0, canvasHeight = 0, plotWidth = 0, plotHeight = 0, hooks = { processOptions: [], @@ -171,7 +624,7 @@ plot.setupGrid = setupGrid; plot.draw = draw; plot.getPlaceholder = function() { return placeholder; }; - plot.getCanvas = function() { return canvas; }; + plot.getCanvas = function() { return surface.element; }; plot.getPlotOffset = function() { return plotOffset; }; plot.width = function () { return plotWidth; }; plot.height = function () { return plotHeight; }; @@ -206,9 +659,10 @@ }; plot.shutdown = shutdown; plot.resize = function () { - getCanvasDimensions(); - resizeCanvas(canvas); - resizeCanvas(overlay); + var width = placeholder.width(), + height = placeholder.height(); + surface.resize(width, height); + overlay.resize(width, height); }; // public attributes @@ -231,40 +685,103 @@ } function initPlugins() { + + // References to key classes, allowing plugins to modify them + + var classes = { + Canvas: Canvas + }; + for (var i = 0; i < plugins.length; ++i) { var p = plugins[i]; - p.init(plot); + p.init(plot, classes); if (p.options) $.extend(true, options, p.options); } } function parseOptions(opts) { - var i; $.extend(true, options, opts); - if (options.xaxis.color == null) - options.xaxis.color = options.grid.color; - if (options.yaxis.color == null) - options.yaxis.color = options.grid.color; + // $.extend merges arrays, rather than replacing them. When less + // colors are provided than the size of the default palette, we + // end up with those colors plus the remaining defaults, which is + // not expected behavior; avoid it by replacing them here. - if (options.xaxis.tickColor == null) // backwards-compatibility - options.xaxis.tickColor = options.grid.tickColor; - if (options.yaxis.tickColor == null) // backwards-compatibility - options.yaxis.tickColor = options.grid.tickColor; + if (opts && opts.colors) { + options.colors = opts.colors; + } + + if (options.xaxis.color == null) + options.xaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + if (options.yaxis.color == null) + options.yaxis.color = $.color.parse(options.grid.color).scale('a', 0.22).toString(); + + if (options.xaxis.tickColor == null) // grid.tickColor for back-compatibility + options.xaxis.tickColor = options.grid.tickColor || options.xaxis.color; + if (options.yaxis.tickColor == null) // grid.tickColor for back-compatibility + options.yaxis.tickColor = options.grid.tickColor || options.yaxis.color; if (options.grid.borderColor == null) options.grid.borderColor = options.grid.color; if (options.grid.tickColor == null) options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString(); - // fill in defaults in axes, copy at least always the - // first as the rest of the code assumes it'll be there - for (i = 0; i < Math.max(1, options.xaxes.length); ++i) - options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]); - for (i = 0; i < Math.max(1, options.yaxes.length); ++i) - options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]); + // Fill in defaults for axis options, including any unspecified + // font-spec fields, if a font-spec was provided. + + // If no x/y axis options were provided, create one of each anyway, + // since the rest of the code assumes that they exist. + + var i, axisOptions, axisCount, + fontDefaults = { + style: placeholder.css("font-style"), + size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), + variant: placeholder.css("font-variant"), + weight: placeholder.css("font-weight"), + family: placeholder.css("font-family") + }; + + fontDefaults.lineHeight = fontDefaults.size * 1.15; + + axisCount = options.xaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.xaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.xaxis, axisOptions); + options.xaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } + + axisCount = options.yaxes.length || 1; + for (i = 0; i < axisCount; ++i) { + + axisOptions = options.yaxes[i]; + if (axisOptions && !axisOptions.tickColor) { + axisOptions.tickColor = axisOptions.color; + } + + axisOptions = $.extend(true, {}, options.yaxis, axisOptions); + options.yaxes[i] = axisOptions; + + if (axisOptions.font) { + axisOptions.font = $.extend({}, fontDefaults, axisOptions.font); + if (!axisOptions.font.color) { + axisOptions.font.color = axisOptions.color; + } + } + } // backwards compatibility, to be removed in future if (options.xaxis.noTicks && options.xaxis.ticks == null) @@ -420,10 +937,10 @@ function fillInSeriesOptions() { - var neededColors = series.length, maxIndex = 0, i; + var neededColors = series.length, maxIndex = -1, i; - // Subtract the number of series that already have fixed - // colors from the number we need to generate. + // Subtract the number of series that already have fixed colors or + // color indexes from the number that we still need to generate. for (i = 0; i < series.length; ++i) { var sc = series[i].color; @@ -435,14 +952,15 @@ } } - // If any of the user colors are numeric indexes, then we - // need to generate at least as many as the highest index. + // If any of the series have fixed color indexes, then we need to + // generate at least as many colors as the highest index. - if (maxIndex > neededColors) { + if (neededColors <= maxIndex) { neededColors = maxIndex + 1; } - // Generate the needed colors, based on the option colors + // Generate all the colors, using first the option colors and then + // variations on those colors once they're exhausted. var c, colors = [], colorPool = options.colors, colorPoolSize = colorPool.length, variation = 0; @@ -496,6 +1014,13 @@ s.lines.show = true; } + // If nothing was provided for lines.zero, default it to match + // lines.fill, since areas by default should extend to zero. + + if (s.lines.zero == null) { + s.lines.zero = !!s.lines.fill; + } + // setup axes s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x")); s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y")); @@ -545,7 +1070,8 @@ format.push({ y: true, number: true, required: true }); if (s.bars.show || (s.lines.show && s.lines.fill)) { - format.push({ y: true, number: true, required: false, defaultValue: 0 }); + var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); + format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); if (s.bars.horizontal) { delete format[format.length - 1].y; format[format.length - 1].x = true; @@ -605,10 +1131,14 @@ if (val != null) { f = format[m]; // extract min/max info - if (f.x) - updateAxis(s.xaxis, val, val); - if (f.y) - updateAxis(s.yaxis, val, val); + if (f.autoscale) { + if (f.x) { + updateAxis(s.xaxis, val, val); + } + if (f.y) { + updateAxis(s.yaxis, val, val); + } + } } points[k + m] = null; } @@ -645,7 +1175,7 @@ // second pass: find datamax/datamin for auto-scaling for (i = 0; i < series.length; ++i) { s = series[i]; - points = s.datapoints.points, + points = s.datapoints.points; ps = s.datapoints.pointsize; format = s.datapoints.format; @@ -659,7 +1189,7 @@ for (m = 0; m < ps; ++m) { val = points[j + m]; f = format[m]; - if (!f || val == fakeInfinity || val == -fakeInfinity) + if (!f || f.autoscale === false || val == fakeInfinity || val == -fakeInfinity) continue; if (f.x) { @@ -717,163 +1247,33 @@ }); } - ////////////////////////////////////////////////////////////////////////////////// - // Returns the display's ratio between physical and device-independent pixels. - // - // This is the ratio between the width that the browser advertises and the number - // of pixels actually available in that space. The iPhone 4, for example, has a - // device-independent width of 320px, but its screen is actually 640px wide. It - // therefore has a pixel ratio of 2, while most normal devices have a ratio of 1. - - function getPixelRatio(cctx) { - var devicePixelRatio = window.devicePixelRatio || 1; - var backingStoreRatio = - cctx.webkitBackingStorePixelRatio || - cctx.mozBackingStorePixelRatio || - cctx.msBackingStorePixelRatio || - cctx.oBackingStorePixelRatio || - cctx.backingStorePixelRatio || 1; - - return devicePixelRatio / backingStoreRatio; - } - - function makeCanvas(skipPositioning, cls) { - - var c = document.createElement('canvas'); - c.className = cls; - - if (!skipPositioning) - $(c).css({ position: 'absolute', left: 0, top: 0 }); - - $(c).appendTo(placeholder); - - // If HTML5 Canvas isn't available, fall back to Excanvas - - if (!c.getContext) { - if (window.G_vmlCanvasManager) { - c = window.G_vmlCanvasManager.initElement(c); - } else { - throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode."); - } - } - - var cctx = c.getContext("2d"); - - // Increase the canvas density based on the display's pixel ratio; basically - // giving the canvas more pixels without increasing the size of its element, - // to take advantage of the fact that retina displays have that many more - // pixels than they actually use for page & element widths. - - var pixelRatio = getPixelRatio(cctx); - - c.width = canvasWidth * pixelRatio; - c.height = canvasHeight * pixelRatio; - c.style.width = canvasWidth + "px"; - c.style.height = canvasHeight + "px"; - - // Save the context so we can reset in case we get replotted - - cctx.save(); - - // Scale the coordinate space to match the display density; so even though we - // may have twice as many pixels, we still want lines and other drawing to - // appear at the same size; the extra pixels will just make them crisper. - - cctx.scale(pixelRatio, pixelRatio); - - return c; - } - - function getCanvasDimensions() { - canvasWidth = placeholder.width(); - canvasHeight = placeholder.height(); - - if (canvasWidth <= 0 || canvasHeight <= 0) - throw new Error("Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight); - } - - function resizeCanvas(c) { - - var cctx = c.getContext("2d"); - - // Handle pixel ratios > 1 for retina displays, as explained in makeCanvas - - var pixelRatio = getPixelRatio(cctx); - - // Resizing should reset the state (excanvas seems to be buggy though) - - if (c.style.width != canvasWidth) { - c.width = canvasWidth * pixelRatio; - c.style.width = canvasWidth + "px"; - } - - if (c.style.height != canvasHeight) { - c.height = canvasHeight * pixelRatio; - c.style.height = canvasHeight + "px"; - } - - // so try to get back to the initial state (even if it's - // gone now, this should be safe according to the spec) - cctx.restore(); - - // and save again - cctx.save(); - - // Apply scaling for retina displays, as explained in makeCanvas - - cctx.scale(pixelRatio, pixelRatio); - } - function setupCanvases() { - var reused, - existingCanvas = placeholder.children("canvas.flot-base"), - existingOverlay = placeholder.children("canvas.flot-overlay"); - if (existingCanvas.length == 0 || existingOverlay == 0) { - // init everything + // Make sure the placeholder is clear of everything except canvases + // from a previous plot in this container that we'll try to re-use. - placeholder.html(""); // make sure placeholder is clear + placeholder.css("padding", 0) // padding messes up the positioning + .children(":not(.flot-base,.flot-overlay)").remove(); - placeholder.css({ padding: 0 }); // padding messes up the positioning + if (placeholder.css("position") == 'static') + placeholder.css("position", "relative"); // for positioning labels and overlay - if (placeholder.css("position") == 'static') - placeholder.css("position", "relative"); // for positioning labels and overlay + surface = new Canvas("flot-base", placeholder); + overlay = new Canvas("flot-overlay", placeholder); // overlay canvas for interactive features - getCanvasDimensions(); - - canvas = makeCanvas(true, "flot-base"); - overlay = makeCanvas(false, "flot-overlay"); // overlay canvas for interactive features - - reused = false; - } - else { - // reuse existing elements - - canvas = existingCanvas.get(0); - overlay = existingOverlay.get(0); - - reused = true; - } - - ctx = canvas.getContext("2d"); - octx = overlay.getContext("2d"); + ctx = surface.context; + octx = overlay.context; // define which element we're listening for events on - eventHolder = $(overlay); + eventHolder = $(overlay.element).unbind(); - if (reused) { - // run shutdown in the old plot object - placeholder.data("plot").shutdown(); + // If we're re-using a plot object, shut down the old one - // reset reused canvases - plot.resize(); + var existing = placeholder.data("plot"); - // make sure overlay pixels are cleared (canvas is cleared when we redraw) - octx.clearRect(0, 0, canvasWidth, canvasHeight); - - // then whack any remaining obvious garbage left - eventHolder.unbind(); - placeholder.children().not([canvas, overlay]).remove(); + if (existing) { + existing.shutdown(); + overlay.clear(); } // save in case we get replotted @@ -884,7 +1284,14 @@ // bind events if (options.grid.hoverable) { eventHolder.mousemove(onMouseMove); - eventHolder.mouseleave(onMouseLeave); + + // Use bind, rather than .mouseleave, because we officially + // still support jQuery 1.2.6, which doesn't define a shortcut + // for mouseenter or mouseleave. This was a bug/oversight that + // was fixed somewhere around 1.3.x. We can return to using + // .mouseleave when we drop support for 1.2.6. + + eventHolder.bind("mouseleave", onMouseLeave); } if (options.grid.clickable) @@ -938,56 +1345,31 @@ } function measureTickLabels(axis) { - var opts = axis.options, ticks = axis.ticks || [], - axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0, - f = axis.font; - ctx.save(); - ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'"; + var opts = axis.options, + ticks = axis.ticks || [], + labelWidth = opts.labelWidth || 0, + labelHeight = opts.labelHeight || 0, + maxWidth = labelWidth || axis.direction == "x" ? Math.floor(surface.width / (ticks.length || 1)) : null; + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = opts.font || "flot-tick-label tickLabel"; for (var i = 0; i < ticks.length; ++i) { - var t = ticks[i]; - t.lines = []; - t.width = t.height = 0; + var t = ticks[i]; if (!t.label) continue; - // accept various kinds of newlines, including HTML ones - // (you can actually split directly on regexps in Javascript, - // but IE is unfortunately broken) - var lines = (t.label + "").replace(/
|\r\n|\r/g, "\n").split("\n"); - for (var j = 0; j < lines.length; ++j) { - var line = { text: lines[j] }, - m = ctx.measureText(line.text); + var info = surface.getTextInfo(layer, t.label, font, null, maxWidth); - line.width = m.width; - // m.height might not be defined, not in the - // standard yet - line.height = m.height != null ? m.height : f.size; - - // add a bit of margin since font rendering is - // not pixel perfect and cut off letters look - // bad, this also doubles as spacing between - // lines - line.height += Math.round(f.size * 0.15); - - t.width = Math.max(line.width, t.width); - t.height += line.height; - - t.lines.push(line); - } - - if (opts.labelWidth == null) - axisw = Math.max(axisw, t.width); - if (opts.labelHeight == null) - axish = Math.max(axish, t.height); + labelWidth = Math.max(labelWidth, info.width); + labelHeight = Math.max(labelHeight, info.height); } - ctx.restore(); - axis.labelWidth = Math.ceil(axisw); - axis.labelHeight = Math.ceil(axish); + axis.labelWidth = opts.labelWidth || labelWidth; + axis.labelHeight = opts.labelHeight || labelHeight; } function allocateAxisBoxFirstPhase(axis) { @@ -1035,7 +1417,7 @@ if (pos == "bottom") { plotOffset.bottom += lh + axisMargin; - axis.box = { top: canvasHeight - plotOffset.bottom, height: lh }; + axis.box = { top: surface.height - plotOffset.bottom, height: lh }; } else { axis.box = { top: plotOffset.top + axisMargin, height: lh }; @@ -1051,7 +1433,7 @@ } else { plotOffset.right += lw + axisMargin; - axis.box = { left: canvasWidth - plotOffset.right, width: lw }; + axis.box = { left: surface.width - plotOffset.right, width: lw }; } } @@ -1067,11 +1449,11 @@ // dimension, we can set the remaining dimension coordinates if (axis.direction == "x") { axis.box.left = plotOffset.left - axis.labelWidth / 2; - axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth; + axis.box.width = surface.width - plotOffset.left - plotOffset.right + axis.labelWidth; } else { axis.box.top = plotOffset.top - axis.labelHeight / 2; - axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight; + axis.box.height = surface.height - plotOffset.bottom - plotOffset.top + axis.labelHeight; } } @@ -1124,10 +1506,10 @@ for (var a in plotOffset) { if(typeof(options.grid.borderWidth) == "object") { - plotOffset[a] = showGrid ? options.grid.borderWidth[a] : 0; + plotOffset[a] += showGrid ? options.grid.borderWidth[a] : 0; } else { - plotOffset[a] = showGrid ? options.grid.borderWidth : 0; + plotOffset[a] += showGrid ? options.grid.borderWidth : 0; } } @@ -1143,14 +1525,6 @@ }); if (showGrid) { - // determine from the placeholder the font size ~ height of font ~ 1 em - var fontDefaults = { - style: placeholder.css("font-style"), - size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)), - variant: placeholder.css("font-variant"), - weight: placeholder.css("font-weight"), - family: placeholder.css("font-family") - }; var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; }); @@ -1159,9 +1533,7 @@ setupTickGeneration(axis); setTicks(axis); snapRangeToTicks(axis, axis.ticks); - // find labelWidth/Height for axis - axis.font = $.extend({}, fontDefaults, axis.options.font); measureTickLabels(axis); }); @@ -1180,14 +1552,18 @@ }); } - plotWidth = canvasWidth - plotOffset.left - plotOffset.right; - plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top; + plotWidth = surface.width - plotOffset.left - plotOffset.right; + plotHeight = surface.height - plotOffset.bottom - plotOffset.top; // now we got the proper plot dimensions, we can compute the scaling $.each(axes, function (_, axis) { setTransformationHelpers(axis); }); + if (showGrid) { + drawAxisLabels(); + } + insertLegend(); } @@ -1240,9 +1616,44 @@ else // heuristic based on the model a*sqrt(x) fitted to // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight); + noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? surface.width : surface.height); - axis.delta = (axis.max - axis.min) / noTicks; + var delta = (axis.max - axis.min) / noTicks, + dec = -Math.floor(Math.log(delta) / Math.LN10), + maxDec = opts.tickDecimals; + + if (maxDec != null && dec > maxDec) { + dec = maxDec; + } + + var magn = Math.pow(10, -dec), + norm = delta / magn, // norm is between 1.0 and 10.0 + size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + if (opts.minTickSize != null && size < opts.minTickSize) { + size = opts.minTickSize; + } + + axis.delta = delta; + axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + axis.tickSize = opts.tickSize || size; // Time mode was moved to a plug-in in 0.8, but since so many people use this // we'll add an especially friendly make sure they remembered to include it. @@ -1256,40 +1667,14 @@ if (!axis.tickGenerator) { - var maxDec = opts.tickDecimals; - var dec = -Math.floor(Math.log(axis.delta) / Math.LN10); - if (maxDec != null && dec > maxDec) - dec = maxDec; - - var magn = Math.pow(10, -dec); - var norm = axis.delta / magn; // norm is between 1.0 and 10.0 - var size; - - if (norm < 1.5) - size = 1; - else if (norm < 3) { - size = 2; - // special case for 2.5, requires an extra decimal - if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { - size = 2.5; - ++dec; - } - } - else if (norm < 7.5) - size = 5; - else size = 10; - - size *= magn; - - if (opts.minTickSize != null && size < opts.minTickSize) - size = opts.minTickSize; - - axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); - axis.tickSize = opts.tickSize || size; - axis.tickGenerator = function (axis) { - var ticks = [], start = floorInBase(axis.min, axis.tickSize), - i = 0, v = Number.NaN, prev; + + var ticks = [], + start = floorInBase(axis.min, axis.tickSize), + i = 0, + v = Number.NaN, + prev; + do { prev = v; v = start + i * axis.tickSize; @@ -1301,7 +1686,7 @@ axis.tickFormatter = function (value, axis) { - var factor = Math.pow(10, axis.tickDecimals); + var factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1; var formatted = "" + Math.round(value * factor) / factor; // If tickDecimals was specified, ensure that we have exactly that @@ -1404,7 +1789,8 @@ } function draw() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); + + surface.clear(); executeHooks(hooks.drawBackground, [ctx]); @@ -1416,7 +1802,6 @@ if (grid.show && !grid.aboveData) { drawGrid(); - drawAxisLabels(); } for (var i = 0; i < series.length; ++i) { @@ -1428,8 +1813,14 @@ if (grid.show && grid.aboveData) { drawGrid(); - drawAxisLabels(); } + + surface.render(); + + // A draw implies that either the axes or data have changed, so we + // should probably update the overlay highlights as well. + + triggerRedrawOverlay(); } function extractRange(ranges, coord) { @@ -1559,7 +1950,6 @@ if (!axis.show || axis.ticks.length == 0) continue; - ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString(); ctx.lineWidth = 1; // find the edges @@ -1580,16 +1970,20 @@ // draw tick bar if (!axis.innermost) { + ctx.strokeStyle = axis.options.color; ctx.beginPath(); xoff = yoff = 0; if (axis.direction == "x") - xoff = plotWidth; + xoff = plotWidth + 1; else - yoff = plotHeight; + yoff = plotHeight + 1; if (ctx.lineWidth == 1) { - x = Math.floor(x) + 0.5; - y = Math.floor(y) + 0.5; + if (axis.direction == "x") { + y = Math.floor(y) + 0.5; + } else { + x = Math.floor(x) + 0.5; + } } ctx.moveTo(x, y); @@ -1598,13 +1992,16 @@ } // draw ticks + + ctx.strokeStyle = axis.options.tickColor; + ctx.beginPath(); for (i = 0; i < axis.ticks.length; ++i) { var v = axis.ticks[i].v; xoff = yoff = 0; - if (v < axis.min || v > axis.max + if (isNaN(v) || v < axis.min || v > axis.max // skip those lying on the axes if we got a border || (t == "full" && ((typeof bw == "object" && bw[axis.position] > 0) || bw > 0) @@ -1701,72 +2098,48 @@ } function drawAxisLabels() { - ctx.save(); $.each(allAxes(), function (_, axis) { if (!axis.show || axis.ticks.length == 0) return; - var box = axis.box, f = axis.font; - // placeholder.append('
') // debug + var box = axis.box, + legacyStyles = axis.direction + "Axis " + axis.direction + axis.n + "Axis", + layer = "flot-" + axis.direction + "-axis flot-" + axis.direction + axis.n + "-axis " + legacyStyles, + font = axis.options.font || "flot-tick-label tickLabel", + tick, x, y, halign, valign; - ctx.fillStyle = axis.options.color; - // Important: Don't use quotes around axis.font.family! Just around single - // font names like 'Times New Roman' that have a space or special character in it. - ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family; - ctx.textAlign = "start"; - // middle align the labels - top would be more - // natural, but browsers can differ a pixel or two in - // where they consider the top to be, so instead we - // middle align to minimize variation between browsers - // and compensate when calculating the coordinates - ctx.textBaseline = "middle"; + surface.removeText(layer); for (var i = 0; i < axis.ticks.length; ++i) { - var tick = axis.ticks[i]; + + tick = axis.ticks[i]; if (!tick.label || tick.v < axis.min || tick.v > axis.max) continue; - var x, y, offset = 0, line; - for (var k = 0; k < tick.lines.length; ++k) { - line = tick.lines[k]; - - if (axis.direction == "x") { - x = plotOffset.left + axis.p2c(tick.v) - line.width/2; - if (axis.position == "bottom") - y = box.top + box.padding; - else - y = box.top + box.height - box.padding - tick.height; + if (axis.direction == "x") { + halign = "center"; + x = plotOffset.left + axis.p2c(tick.v); + if (axis.position == "bottom") { + y = box.top + box.padding; + } else { + y = box.top + box.height - box.padding; + valign = "bottom"; } - else { - y = plotOffset.top + axis.p2c(tick.v) - tick.height/2; - if (axis.position == "left") - x = box.left + box.width - box.padding - line.width; - else - x = box.left + box.padding; + } else { + valign = "middle"; + y = plotOffset.top + axis.p2c(tick.v); + if (axis.position == "left") { + x = box.left + box.width - box.padding; + halign = "right"; + } else { + x = box.left + box.padding; } - - // account for middle aligning and line number - y += line.height/2 + offset; - offset += line.height; - - if ($.browser.opera) { - // FIXME: UGLY BROWSER DETECTION - // round the coordinates since Opera - // otherwise switches to more ugly - // rendering (probably non-hinted) and - // offset the y coordinates since it seems - // to be off pretty consistently compared - // to the other browsers - x = Math.floor(x); - y = Math.ceil(y - 2); - } - ctx.fillText(line.text, x, y); } + + surface.addText(layer, x, y, tick.label, font, null, null, halign, valign); } }); - - ctx.restore(); } function drawSeries(series) { @@ -2065,6 +2438,15 @@ sw = series.shadowSize, radius = series.points.radius, symbol = series.points.symbol; + + // If the user sets the line width to 0, we change it to a very + // small value. A line width of 0 seems to force the default of 1. + // Doing the conditional here allows the shadow setting to still be + // optional even with a lineWidth of 0. + + if( lw == 0 ) + lw = 0.0001; + if (lw > 0 && sw > 0) { // draw shadow in two steps var w = sw / 2; @@ -2279,6 +2661,8 @@ if (options.legend.sorted) { if ($.isFunction(options.legend.sorted)) { entries.sort(options.legend.sorted); + } else if (options.legend.sorted == "reverse") { + entries.reverse(); } else { var ascending = options.legend.sorted != "descending"; entries.sort(function(a, b) { @@ -2293,7 +2677,7 @@ for (var i = 0; i < entries.length; ++i) { - entry = entries[i]; + var entry = entries[i]; if (i % options.legend.noColumns == 0) { if (rowStarted) @@ -2449,7 +2833,7 @@ function onMouseMove(e) { if (options.grid.hoverable) triggerClickHoverEvent("plothover", e, - function (s) { return !!s["hoverable"]; }); + function (s) { return s["hoverable"] != false; }); } function onMouseLeave(e) { @@ -2460,7 +2844,7 @@ function onClick(e) { triggerClickHoverEvent("plotclick", e, - function (s) { return !!s["clickable"]; }); + function (s) { return s["clickable"] != false; }); } // trigger click or hover event (they send the same parameters @@ -2516,7 +2900,7 @@ // draw highlights octx.save(); - octx.clearRect(0, 0, canvasWidth, canvasHeight); + overlay.clear(); octx.translate(plotOffset.left, plotOffset.top); var i, hi; @@ -2556,13 +2940,16 @@ if (s == null && point == null) { highlights = []; triggerRedrawOverlay(); + return; } if (typeof s == "number") s = series[s]; - if (typeof point == "number") - point = s.data[point]; + if (typeof point == "number") { + var ps = s.datapoints.pointsize; + point = s.datapoints.points.slice(ps * point, ps * (point + 1)); + } var i = indexOfHighlight(s, point); if (i != -1) { @@ -2584,7 +2971,7 @@ function drawPointHighlight(series, point) { var x = point[0], y = point[1], - axisx = series.xaxis, axisy = series.yaxis; + axisx = series.xaxis, axisy = series.yaxis, highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(); if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) @@ -2645,6 +3032,8 @@ } } + // Add the plot function to the top level of the jQuery object + $.plot = function(placeholder, data, options) { //var t0 = new Date(); var plot = new Plot($(placeholder), data, options, $.plot.plugins); @@ -2652,10 +3041,18 @@ return plot; }; - $.plot.version = "0.7"; + $.plot.version = "0.8.1"; $.plot.plugins = []; + // Also add the plot function as a chainable property + + $.fn.plot = function(data, options) { + return this.each(function() { + $.plot(this, data, options); + }); + }; + // round to nearby lower multiple of base function floorInBase(n, base) { return base * Math.floor(n / base); diff --git a/vendor/flot/jquery.flot.time.js b/vendor/flot/jquery.flot.time.js new file mode 100644 index 00000000..15f52815 --- /dev/null +++ b/vendor/flot/jquery.flot.time.js @@ -0,0 +1,431 @@ +/* Pretty handling of time axes. + +Copyright (c) 2007-2013 IOLA and Ole Laursen. +Licensed under the MIT license. + +Set axis.mode to "time" to enable. See the section "Time series data" in +API.txt for details. + +*/ + +(function($) { + + var options = { + xaxis: { + timezone: null, // "browser" for local to the client or timezone for timezone-js + timeformat: null, // format string to use + twelveHourClock: false, // 12 or 24 time in time mode + monthNames: null // list of names of months + } + }; + + // round to nearby lower multiple of base + + function floorInBase(n, base) { + return base * Math.floor(n / base); + } + + // Returns a string with the date d formatted according to fmt. + // A subset of the Open Group's strftime format is supported. + + function formatDate(d, fmt, monthNames, dayNames) { + + if (typeof d.strftime == "function") { + return d.strftime(fmt); + } + + var leftPad = function(n, pad) { + n = "" + n; + pad = "" + (pad == null ? "0" : pad); + return n.length == 1 ? pad + n : n; + }; + + var r = []; + var escape = false; + var hours = d.getHours(); + var isAM = hours < 12; + + if (monthNames == null) { + monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + } + + if (dayNames == null) { + dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + } + + var hours12; + + if (hours > 12) { + hours12 = hours - 12; + } else if (hours == 0) { + hours12 = 12; + } else { + hours12 = hours; + } + + for (var i = 0; i < fmt.length; ++i) { + + var c = fmt.charAt(i); + + if (escape) { + switch (c) { + case 'a': c = "" + dayNames[d.getDay()]; break; + case 'b': c = "" + monthNames[d.getMonth()]; break; + case 'd': c = leftPad(d.getDate()); break; + case 'e': c = leftPad(d.getDate(), " "); break; + case 'h': // For back-compat with 0.7; remove in 1.0 + case 'H': c = leftPad(hours); break; + case 'I': c = leftPad(hours12); break; + case 'l': c = leftPad(hours12, " "); break; + case 'm': c = leftPad(d.getMonth() + 1); break; + case 'M': c = leftPad(d.getMinutes()); break; + // quarters not in Open Group's strftime specification + case 'q': + c = "" + (Math.floor(d.getMonth() / 3) + 1); break; + case 'S': c = leftPad(d.getSeconds()); break; + case 'y': c = leftPad(d.getFullYear() % 100); break; + case 'Y': c = "" + d.getFullYear(); break; + case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break; + case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break; + case 'w': c = "" + d.getDay(); break; + } + r.push(c); + escape = false; + } else { + if (c == "%") { + escape = true; + } else { + r.push(c); + } + } + } + + return r.join(""); + } + + // To have a consistent view of time-based data independent of which time + // zone the client happens to be in we need a date-like object independent + // of time zones. This is done through a wrapper that only calls the UTC + // versions of the accessor methods. + + function makeUtcWrapper(d) { + + function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) { + sourceObj[sourceMethod] = function() { + return targetObj[targetMethod].apply(targetObj, arguments); + }; + }; + + var utc = { + date: d + }; + + // support strftime, if found + + if (d.strftime != undefined) { + addProxyMethod(utc, "strftime", d, "strftime"); + } + + addProxyMethod(utc, "getTime", d, "getTime"); + addProxyMethod(utc, "setTime", d, "setTime"); + + var props = ["Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds"]; + + for (var p = 0; p < props.length; p++) { + addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]); + addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]); + } + + return utc; + }; + + // select time zone strategy. This returns a date-like object tied to the + // desired timezone + + function dateGenerator(ts, opts) { + if (opts.timezone == "browser") { + return new Date(ts); + } else if (!opts.timezone || opts.timezone == "utc") { + return makeUtcWrapper(new Date(ts)); + } else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") { + var d = new timezoneJS.Date(); + // timezone-js is fickle, so be sure to set the time zone before + // setting the time. + d.setTimezone(opts.timezone); + d.setTime(ts); + return d; + } else { + return makeUtcWrapper(new Date(ts)); + } + } + + // map of app. size of time units in milliseconds + + var timeUnitSize = { + "second": 1000, + "minute": 60 * 1000, + "hour": 60 * 60 * 1000, + "day": 24 * 60 * 60 * 1000, + "month": 30 * 24 * 60 * 60 * 1000, + "quarter": 3 * 30 * 24 * 60 * 60 * 1000, + "year": 365.2425 * 24 * 60 * 60 * 1000 + }; + + // the allowed tick sizes, after 1 year we use + // an integer algorithm + + var baseSpec = [ + [1, "second"], [2, "second"], [5, "second"], [10, "second"], + [30, "second"], + [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], + [30, "minute"], + [1, "hour"], [2, "hour"], [4, "hour"], + [8, "hour"], [12, "hour"], + [1, "day"], [2, "day"], [3, "day"], + [0.25, "month"], [0.5, "month"], [1, "month"], + [2, "month"] + ]; + + // we don't know which variant(s) we'll need yet, but generating both is + // cheap + + var specMonths = baseSpec.concat([[3, "month"], [6, "month"], + [1, "year"]]); + var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"], + [1, "year"]]); + + function init(plot) { + plot.hooks.processOptions.push(function (plot, options) { + $.each(plot.getAxes(), function(axisName, axis) { + + var opts = axis.options; + + if (opts.mode == "time") { + axis.tickGenerator = function(axis) { + + var ticks = []; + var d = dateGenerator(axis.min, opts); + var minSize = 0; + + // make quarter use a possibility if quarters are + // mentioned in either of these options + + var spec = (opts.tickSize && opts.tickSize[1] === + "quarter") || + (opts.minTickSize && opts.minTickSize[1] === + "quarter") ? specQuarters : specMonths; + + if (opts.minTickSize != null) { + if (typeof opts.tickSize == "number") { + minSize = opts.tickSize; + } else { + minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]]; + } + } + + for (var i = 0; i < spec.length - 1; ++i) { + if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] + + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 + && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) { + break; + } + } + + var size = spec[i][0]; + var unit = spec[i][1]; + + // special-case the possibility of several years + + if (unit == "year") { + + // if given a minTickSize in years, just use it, + // ensuring that it's an integer + + if (opts.minTickSize != null && opts.minTickSize[1] == "year") { + size = Math.floor(opts.minTickSize[0]); + } else { + + var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10)); + var norm = (axis.delta / timeUnitSize.year) / magn; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + } + + // minimum size for years is 1 + + if (size < 1) { + size = 1; + } + } + + axis.tickSize = opts.tickSize || [size, unit]; + var tickSize = axis.tickSize[0]; + unit = axis.tickSize[1]; + + var step = tickSize * timeUnitSize[unit]; + + if (unit == "second") { + d.setSeconds(floorInBase(d.getSeconds(), tickSize)); + } else if (unit == "minute") { + d.setMinutes(floorInBase(d.getMinutes(), tickSize)); + } else if (unit == "hour") { + d.setHours(floorInBase(d.getHours(), tickSize)); + } else if (unit == "month") { + d.setMonth(floorInBase(d.getMonth(), tickSize)); + } else if (unit == "quarter") { + d.setMonth(3 * floorInBase(d.getMonth() / 3, + tickSize)); + } else if (unit == "year") { + d.setFullYear(floorInBase(d.getFullYear(), tickSize)); + } + + // reset smaller components + + d.setMilliseconds(0); + + if (step >= timeUnitSize.minute) { + d.setSeconds(0); + } + if (step >= timeUnitSize.hour) { + d.setMinutes(0); + } + if (step >= timeUnitSize.day) { + d.setHours(0); + } + if (step >= timeUnitSize.day * 4) { + d.setDate(1); + } + if (step >= timeUnitSize.month * 2) { + d.setMonth(floorInBase(d.getMonth(), 3)); + } + if (step >= timeUnitSize.quarter * 2) { + d.setMonth(floorInBase(d.getMonth(), 6)); + } + if (step >= timeUnitSize.year) { + d.setMonth(0); + } + + var carry = 0; + var v = Number.NaN; + var prev; + + do { + + prev = v; + v = d.getTime(); + ticks.push(v); + + if (unit == "month" || unit == "quarter") { + if (tickSize < 1) { + + // a bit complicated - we'll divide the + // month/quarter up but we need to take + // care of fractions so we don't end up in + // the middle of a day + + d.setDate(1); + var start = d.getTime(); + d.setMonth(d.getMonth() + + (unit == "quarter" ? 3 : 1)); + var end = d.getTime(); + d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize); + carry = d.getHours(); + d.setHours(0); + } else { + d.setMonth(d.getMonth() + + tickSize * (unit == "quarter" ? 3 : 1)); + } + } else if (unit == "year") { + d.setFullYear(d.getFullYear() + tickSize); + } else { + d.setTime(v + step); + } + } while (v < axis.max && v != prev); + + return ticks; + }; + + axis.tickFormatter = function (v, axis) { + + var d = dateGenerator(v, axis.options); + + // first check global format + + if (opts.timeformat != null) { + return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames); + } + + // possibly use quarters if quarters are mentioned in + // any of these places + + var useQuarters = (axis.options.tickSize && + axis.options.tickSize[1] == "quarter") || + (axis.options.minTickSize && + axis.options.minTickSize[1] == "quarter"); + + var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]]; + var span = axis.max - axis.min; + var suffix = (opts.twelveHourClock) ? " %p" : ""; + var hourCode = (opts.twelveHourClock) ? "%I" : "%H"; + var fmt; + + if (t < timeUnitSize.minute) { + fmt = hourCode + ":%M:%S" + suffix; + } else if (t < timeUnitSize.day) { + if (span < 2 * timeUnitSize.day) { + fmt = hourCode + ":%M" + suffix; + } else { + fmt = "%b %d " + hourCode + ":%M" + suffix; + } + } else if (t < timeUnitSize.month) { + fmt = "%b %d"; + } else if ((useQuarters && t < timeUnitSize.quarter) || + (!useQuarters && t < timeUnitSize.year)) { + if (span < timeUnitSize.year) { + fmt = "%b"; + } else { + fmt = "%b %Y"; + } + } else if (useQuarters && t < timeUnitSize.year) { + if (span < timeUnitSize.year) { + fmt = "Q%q"; + } else { + fmt = "Q%q %Y"; + } + } else { + fmt = "%Y"; + } + + var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames); + + return rt; + }; + } + }); + }); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'time', + version: '1.0' + }); + + // Time-axis support used to be in Flot core, which exposed the + // formatDate function on the plot object. Various plugins depend + // on the function, so we need to re-expose it here. + + $.plot.formatDate = formatDate; + +})(jQuery); From 39ec742d425058eb80909c410f3ab8570d7dc47b Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 18:42:25 +0100 Subject: [PATCH 31/74] [view/flot][xs]: cast dates to strings before attempting to parse. --- src/view.flot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index 5059156e..43d997a7 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -316,7 +316,8 @@ my.Flot = Backbone.View.extend({ var x = doc.getFieldValue(xfield); if (isDateTime) { - var _date = moment(x); + // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 + var _date = moment(String(x)); if (_date.isValid()) { x = _date.toDate().getTime(); } From b297f2e25a420907211eab0308525b8b00f46e94 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 18:43:04 +0100 Subject: [PATCH 32/74] [build][s]: regular build. --- dist/recline.js | 79 +++++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index 03a5f7f1..c9d4a7b2 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -1871,25 +1871,17 @@ my.Flot = Backbone.View.extend({ }, _xaxisLabel: function (x) { - var xfield = this.model.fields.get(this.state.attributes.group); - - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - - if (this.xvaluesAreIndex) { + if (this._groupFieldIsDateTime()) { + // oddly x comes through as milliseconds *string* (rather than int + // or float) so we have to reparse + x = new Date(parseFloat(x)).toLocaleDateString(); + } else if (this.xvaluesAreIndex) { x = parseInt(x, 10); // HACK: deal with bar graph style cases where x-axis items were strings // In this case x at this point is the index of the item in the list of // records not its actual x-axis value x = this.model.records.models[x].get(this.state.attributes.group); } - if (isDateTime) { - x = new Date(x).toLocaleDateString(); - } - // } else if (isDateTime) { - // x = new Date(parseInt(x, 10)).toLocaleDateString(); - // } return x; }, @@ -1904,25 +1896,26 @@ my.Flot = Backbone.View.extend({ // @param numPoints the number of points that will be plotted getGraphOptions: function(typeId, numPoints) { var self = this; - - var tickFormatter = function (x) { - // convert x to a string and make sure that it is not too long or the - // tick labels will overlap - // TODO: find a more accurate way of calculating the size of tick labels - var label = self._xaxisLabel(x) || ""; - - if (typeof label !== 'string') { - label = label.toString(); - } - if (self.state.attributes.graphType !== 'bars' && label.length > 10) { - label = label.slice(0, 10) + "..."; - } - - return label; - }; - + var groupFieldIsDateTime = self._groupFieldIsDateTime(); var xaxis = {}; - xaxis.tickFormatter = tickFormatter; + + if (!groupFieldIsDateTime) { + xaxis.tickFormatter = function (x) { + // convert x to a string and make sure that it is not too long or the + // tick labels will overlap + // TODO: find a more accurate way of calculating the size of tick labels + var label = self._xaxisLabel(x) || ""; + + if (typeof label !== 'string') { + label = label.toString(); + } + if (self.state.attributes.graphType !== 'bars' && label.length > 10) { + label = label.slice(0, 10) + "..."; + } + + return label; + }; + } // for labels case we only want ticks at the label intervals // HACK: however we also get this case with Date fields. In that case we @@ -1935,6 +1928,8 @@ my.Flot = Backbone.View.extend({ ticks.push(parseInt(i*increment, 10)); } xaxis.ticks = ticks; + } else if (groupFieldIsDateTime) { + xaxis.mode = 'time'; } var yaxis = {}; @@ -2016,24 +2011,32 @@ my.Flot = Backbone.View.extend({ } }, + _groupFieldIsDateTime: function() { + var xfield = this.model.fields.get(this.state.attributes.group); + var xtype = xfield.get('type'); + var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); + return isDateTime; + }, + createSeries: function() { var self = this; self.xvaluesAreIndex = false; var series = []; + var xfield = self.model.fields.get(self.state.attributes.group); + var isDateTime = self._groupFieldIsDateTime(); + _.each(this.state.attributes.series, function(field) { var points = []; var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { - var xfield = self.model.fields.get(self.state.attributes.group); var x = doc.getFieldValue(xfield); - // time series - var xtype = xfield.get('type'); - var isDateTime = (xtype === 'date' || xtype === 'date-time' || xtype === 'time'); - if (isDateTime) { - self.xvaluesAreIndex = true; - x = index; + // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 + var _date = moment(String(x)); + if (_date.isValid()) { + x = _date.toDate().getTime(); + } } else if (typeof x === 'string') { x = parseFloat(x); if (isNaN(x)) { // assume this is a string label From b4f4c989cc8f68dabca62b5b683a19bb9b282b77 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 19:03:42 +0100 Subject: [PATCH 33/74] [view/multiview][s]: do not call fetch on dataset model in MultiView initialize (should be done in code using the view if needed). * fetch method should have been done elsewhere (by client of the view) * calling in the view is both wasteful and causes possible bugs as it may lead to race conditions to a fetch called by client code --- src/view.multiview.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/view.multiview.js b/src/view.multiview.js index 46f027b1..ccd35171 100644 --- a/src/view.multiview.js +++ b/src/view.multiview.js @@ -228,10 +228,6 @@ my.MultiView = Backbone.View.extend({ // note this.model and dataset returned are the same // TODO: set query state ...? this.model.queryState.set(self.state.get('query'), {silent: true}); - this.model.fetch() - .fail(function(error) { - self.notify({message: error.message, category: 'error', persist: true}); - }); }, setReadOnly: function() { From 26a2a36c6010982d0dcf2a07355edc1e9b00c671 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 19:06:17 +0100 Subject: [PATCH 34/74] [build][xs]: build. --- dist/recline.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index c9d4a7b2..71e57b19 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -3367,10 +3367,6 @@ my.MultiView = Backbone.View.extend({ // note this.model and dataset returned are the same // TODO: set query state ...? this.model.queryState.set(self.state.get('query'), {silent: true}); - this.model.fetch() - .fail(function(error) { - self.notify({message: error.message, category: 'error', persist: true}); - }); }, setReadOnly: function() { From 4d128af797546e5174537d8ed530a243e444eeab Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 20:04:09 +0100 Subject: [PATCH 35/74] [model,bugfix][s]: if explicitly passed field info to a dataset use that over any field info derived from backend. --- src/model.js | 8 +++++++- test/model.test.js | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/model.js b/src/model.js index 40f92532..5649b1d8 100644 --- a/src/model.js +++ b/src/model.js @@ -71,7 +71,13 @@ my.Dataset = Backbone.Model.extend({ } function handleResults(results) { - var out = self._normalizeRecordsAndFields(results.records, results.fields); + // if explicitly given the fields + // (e.g. var dataset = new Dataset({fields: fields, ...}) + // use that field info over anything we get back by parsing the data + // (results.fields) + var fields = self.get('fields') || results.fields; + + var out = self._normalizeRecordsAndFields(results.records, fields); if (results.useMemoryStore) { self._store = new recline.Backend.Memory.Store(out.records, out.fields); } diff --git a/test/model.test.js b/test/model.test.js index e3179dac..42f9c436 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -177,6 +177,25 @@ test('Dataset getFieldsSummary', function () { }); }); +test('fetch without and with explicit fields', function () { + var dataset = new recline.Model.Dataset({ + backend: 'csv', + data: 'A,B\n1,2\n3,4' + }); + dataset.fetch(); + equal(dataset.fields.at(0).id, 'A'); + equal(dataset.fields.at(0).get('type'), 'string'); + + var dataset = new recline.Model.Dataset({ + fields: [{id: 'X', type: 'number'}, {id: 'Y'}], + backend: 'csv', + data: 'A,B\n1,2\n3,4' + }); + dataset.fetch(); + equal(dataset.fields.at(0).id, 'X'); + equal(dataset.fields.at(0).get('type'), 'number'); +}); + test('_normalizeRecordsAndFields', function () { var data = [ // fields but no records From efe55b062c8573b426da2c995dd37b6a4791010e Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 2 Jun 2013 20:04:52 +0100 Subject: [PATCH 36/74] [build][xs]: . --- dist/recline.dataset.js | 8 +++++++- dist/recline.js | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dist/recline.dataset.js b/dist/recline.dataset.js index 2f9946d6..fd97e552 100644 --- a/dist/recline.dataset.js +++ b/dist/recline.dataset.js @@ -71,7 +71,13 @@ my.Dataset = Backbone.Model.extend({ } function handleResults(results) { - var out = self._normalizeRecordsAndFields(results.records, results.fields); + // if explicitly given the fields + // (e.g. var dataset = new Dataset({fields: fields, ...}) + // use that field info over anything we get back by parsing the data + // (results.fields) + var fields = self.get('fields') || results.fields; + + var out = self._normalizeRecordsAndFields(results.records, fields); if (results.useMemoryStore) { self._store = new recline.Backend.Memory.Store(out.records, out.fields); } diff --git a/dist/recline.js b/dist/recline.js index 71e57b19..1fa38c4e 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -1199,7 +1199,13 @@ my.Dataset = Backbone.Model.extend({ } function handleResults(results) { - var out = self._normalizeRecordsAndFields(results.records, results.fields); + // if explicitly given the fields + // (e.g. var dataset = new Dataset({fields: fields, ...}) + // use that field info over anything we get back by parsing the data + // (results.fields) + var fields = self.get('fields') || results.fields; + + var out = self._normalizeRecordsAndFields(results.records, fields); if (results.useMemoryStore) { self._store = new recline.Backend.Memory.Store(out.records, out.fields); } From 379ed118117485ab318dbba3483fa20a21fe4a2a Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 9 Jun 2013 17:04:18 +0100 Subject: [PATCH 37/74] [view/flot,bugfix][s]: use getFieldValueUnrendered for getting values in flot graph. * In case where we render value (e.g. number percentage) use of getFieldValue will result in percentage sign which flot will choke on. --- src/view.flot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index 43d997a7..f20a3ab3 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -313,7 +313,7 @@ my.Flot = Backbone.View.extend({ var points = []; var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { - var x = doc.getFieldValue(xfield); + var x = doc.getFieldValueUnrendered(xfield); if (isDateTime) { // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 @@ -330,7 +330,7 @@ my.Flot = Backbone.View.extend({ } var yfield = self.model.fields.get(field); - var y = doc.getFieldValue(yfield); + var y = doc.getFieldValueUnrendered(yfield); if (self.state.attributes.graphType == 'bars') { points.push([y, x]); From fa2e224bc946b7f461948735b9b60ec43c38d0bf Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 9 Jun 2013 17:05:53 +0100 Subject: [PATCH 38/74] [build][xs]: build. --- dist/recline.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index 1fa38c4e..78690322 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -2035,7 +2035,7 @@ my.Flot = Backbone.View.extend({ var points = []; var fieldLabel = self.model.fields.get(field).get('label'); _.each(self.model.records.models, function(doc, index) { - var x = doc.getFieldValue(xfield); + var x = doc.getFieldValueUnrendered(xfield); if (isDateTime) { // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 @@ -2052,7 +2052,7 @@ my.Flot = Backbone.View.extend({ } var yfield = self.model.fields.get(field); - var y = doc.getFieldValue(yfield); + var y = doc.getFieldValueUnrendered(yfield); if (self.state.attributes.graphType == 'bars') { points.push([y, x]); From 5d24fd474d6e6bd065039d058ef64c6db4454df9 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 15 Jun 2013 12:12:15 +0100 Subject: [PATCH 39/74] [csv,bugfix][s]: fix to csv backend to correct issue with header row appearing in data in case where fields passed explicitly. * The bug was triggered by change in 4d128af797546e5174537d8ed530a243e444eeab to use model fields if explicitly provided * model.test.js illustrated issue (that new test was failing prior to changes to csv backend in this commit) * Issue was that CSV backend did not pass back fields option. Previously, if no fields provided from backend we extracted fields from first row of the returned records. With change in 4d128af797546e5174537d8ed530a243e444eeab to use model fields if provided we had an issue as we no longer removed first row for fields. * Fixed by having CSV backend explicitly extract fields and pass them back --- src/backend.csv.js | 46 ++++++++++++++++++++++++---------------- test/backend.csv.test.js | 13 ++++++++++++ test/model.test.js | 1 + 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/backend.csv.js b/src/backend.csv.js index 28d58c2b..e5a8a599 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -33,37 +33,45 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var reader = new FileReader(); var encoding = dataset.encoding || 'UTF-8'; reader.onload = function(e) { - var rows = my.parseCSV(e.target.result, dataset); - dfd.resolve({ - records: rows, - metadata: { - filename: dataset.file.name - }, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(data, dataset), dataset); + out.useMemoryStore = true; + out.metadata = { + filename: dataset.file.name + } + dfd.resolve(out); }; reader.onerror = function (e) { alert('Failed to load file. Code: ' + e.target.error.code); }; reader.readAsText(dataset.file, encoding); } else if (dataset.data) { - var rows = my.parseCSV(dataset.data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(dataset.data, dataset), dataset); + out.useMemoryStore = true; + dfd.resolve(out); } else if (dataset.url) { jQuery.get(dataset.url).done(function(data) { - var rows = my.parseCSV(data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(data, dataset), dataset); + out.useMemoryStore = true; }); } return dfd.promise(); }; + // Convert array of rows in { records: [ ...] , fields: [ ... ] } + // @param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data. + my.extractFields = function(rows, noFields) { + if (noFields.noHeaderRow !== true && rows.length > 0) { + return { + fields: rows[0], + records: rows.slice(1) + } + } else { + return { + records: rows + } + } + }; + // ## parseCSV // // Converts a Comma Separated Values string into an array of arrays. @@ -84,6 +92,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // fields containing special characters, such as the delimiter or // quotechar, or which contain new-line characters. It defaults to '"' // + // @param {Integer} skipInitialRows A integer number of rows to skip (default 0) + // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parseCSV= function(s, options) { diff --git a/test/backend.csv.test.js b/test/backend.csv.test.js index c027f097..e7b09b72 100644 --- a/test/backend.csv.test.js +++ b/test/backend.csv.test.js @@ -64,6 +64,19 @@ test("parseCSV - quotechar", function() { }); +test("parseCSV skipInitialRows", function() { + var csv = '"Jones, Jay",10\n' + + '"Xyz ""ABC"" O\'Brien",11:35\n' + + '"Other, AN",12:35\n'; + + var array = recline.Backend.CSV.parseCSV(csv, {skipInitialRows: 1}); + var exp = [ + ['Xyz "ABC" O\'Brien', '11:35' ], + ['Other, AN', '12:35' ] + ]; + deepEqual(exp, array); +}); + test("serializeCSV - Array", function() { var csv = [ ['Jones, Jay', 10], diff --git a/test/model.test.js b/test/model.test.js index 42f9c436..00797ef0 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -194,6 +194,7 @@ test('fetch without and with explicit fields', function () { dataset.fetch(); equal(dataset.fields.at(0).id, 'X'); equal(dataset.fields.at(0).get('type'), 'number'); + equal(dataset.records.at(0).get('X'), 1); }); test('_normalizeRecordsAndFields', function () { From 9a7e78f686793f9186c1182561a382072eab0ae9 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 15 Jun 2013 12:19:36 +0100 Subject: [PATCH 40/74] [build][xs]: cat files. --- dist/recline.js | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index 78690322..c660fdb3 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -33,37 +33,45 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var reader = new FileReader(); var encoding = dataset.encoding || 'UTF-8'; reader.onload = function(e) { - var rows = my.parseCSV(e.target.result, dataset); - dfd.resolve({ - records: rows, - metadata: { - filename: dataset.file.name - }, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(data, dataset), dataset); + out.useMemoryStore = true; + out.metadata = { + filename: dataset.file.name + } + dfd.resolve(out); }; reader.onerror = function (e) { alert('Failed to load file. Code: ' + e.target.error.code); }; reader.readAsText(dataset.file, encoding); } else if (dataset.data) { - var rows = my.parseCSV(dataset.data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(dataset.data, dataset), dataset); + out.useMemoryStore = true; + dfd.resolve(out); } else if (dataset.url) { jQuery.get(dataset.url).done(function(data) { - var rows = my.parseCSV(data, dataset); - dfd.resolve({ - records: rows, - useMemoryStore: true - }); + var out = my.extractFields(my.parseCSV(data, dataset), dataset); + out.useMemoryStore = true; }); } return dfd.promise(); }; + // Convert array of rows in { records: [ ...] , fields: [ ... ] } + // @param {Boolean} noHeaderRow If true assume that first row is not a header (i.e. list of fields but is data. + my.extractFields = function(rows, noFields) { + if (noFields.noHeaderRow !== true && rows.length > 0) { + return { + fields: rows[0], + records: rows.slice(1) + } + } else { + return { + records: rows + } + } + }; + // ## parseCSV // // Converts a Comma Separated Values string into an array of arrays. @@ -84,6 +92,8 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; // fields containing special characters, such as the delimiter or // quotechar, or which contain new-line characters. It defaults to '"' // + // @param {Integer} skipInitialRows A integer number of rows to skip (default 0) + // // Heavily based on uselesscode's JS CSV parser (MIT Licensed): // http://www.uselesscode.org/javascript/csv/ my.parseCSV= function(s, options) { From 323f5febdd0fe6c2168a7f790c3d81a654a882cd Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 22 Jun 2013 18:03:03 +0100 Subject: [PATCH 41/74] [test][xs]: add marker css to test index.html. --- test/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/index.html b/test/index.html index 9cf784c2..ddb4aa92 100644 --- a/test/index.html +++ b/test/index.html @@ -7,6 +7,8 @@ + + From 35a5f321634e82890dcb0c3f836c86fa3258bf52 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 22 Jun 2013 18:14:50 +0100 Subject: [PATCH 42/74] [#362,view/map][s]: disable auto-turn on of map clustering above a certain number of points - fixes #362. --- docs/tutorial-maps.markdown | 27 +++++++++++++++++++++++++++ src/view.map.js | 11 ++--------- test/view.map.test.js | 8 +++++++- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/docs/tutorial-maps.markdown b/docs/tutorial-maps.markdown index 914d7d52..60ab0a1d 100644 --- a/docs/tutorial-maps.markdown +++ b/docs/tutorial-maps.markdown @@ -102,3 +102,30 @@ view.featureparse = function (e) { }; {% endhighlight %} + +### Turning on clustering + +You can turn on clustering of markers by setting the cluster option: + + var map = new recline.View.Map({ + model: dataset + state: { + cluster: true; + } + }); + +You could also enable marker clustering only if you have more than a +certain number of markers. Here's an example: + + // var map is recline.View.Map instance + // marker cluster threshold + var threshold = 65; + + // enable clustering if there is a large number of markers + var countAfter = 0; + map.features.eachLayer(function(){countAfter++;}); + if (countAfter > threshold) { + // note the map will auto-redraw when you change state! + map.state.set({cluster: true}); + } + diff --git a/src/view.map.js b/src/view.map.js index 23d02e25..ed52590d 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -28,6 +28,8 @@ this.recline.View = this.recline.View || {}; // latField: {id of field containing latitude in the dataset} // autoZoom: true, // // use cluster support +// // cluster: true = always on +// // cluster: false = always off // cluster: false // } // @@ -217,15 +219,6 @@ my.Map = Backbone.View.extend({ this._remove(doc); } - // enable clustering if there is a large number of markers - var countAfter = 0; - this.features.eachLayer(function(){countAfter++;}); - var sizeIncreased = countAfter - countBefore > 0; - if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) { - this.state.set({cluster: true}); - return; - } - // this must come before zooming! // if not: errors when using e.g. circle markers like // "Cannot call method 'project' of undefined" diff --git a/test/view.map.test.js b/test/view.map.test.js index 598c1a05..e3af2581 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -130,7 +130,7 @@ test('_getGeometryFromRecord non-GeoJSON', function () { }); }); -test('many markers', function () { +test('many markers and clustering', function () { var data = []; for (var i = 0; i<1000; i++) { data.push({ id: i, lon: 13+3*i, lat: 52+i/10}); @@ -150,7 +150,13 @@ test('many markers', function () { dataset.query(); + // this whole test looks a bit odd now + // we used to turn on clustering automatically at a certain level but we do not any more + equal(view.state.get('cluster'), false); + + view.state.set({cluster: true}); equal(view.state.get('cluster'), true); + view.remove(); }); From 005527f2371768e836504aa8e8f0ae78fa66afec Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 22 Jun 2013 18:16:09 +0100 Subject: [PATCH 43/74] [build][xs]: . --- dist/recline.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index c660fdb3..70093f41 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -2531,6 +2531,8 @@ this.recline.View = this.recline.View || {}; // latField: {id of field containing latitude in the dataset} // autoZoom: true, // // use cluster support +// // cluster: true = always on +// // cluster: false = always off // cluster: false // } // @@ -2720,15 +2722,6 @@ my.Map = Backbone.View.extend({ this._remove(doc); } - // enable clustering if there is a large number of markers - var countAfter = 0; - this.features.eachLayer(function(){countAfter++;}); - var sizeIncreased = countAfter - countBefore > 0; - if (!this.state.get('cluster') && countAfter > 64 && sizeIncreased) { - this.state.set({cluster: true}); - return; - } - // this must come before zooming! // if not: errors when using e.g. circle markers like // "Cannot call method 'project' of undefined" From b66f5633689085e14231dcd263f9978e88d7f70a Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 22 Jun 2013 20:14:20 +0100 Subject: [PATCH 44/74] [be/csv,bugfix][xs]: fix to breakage of csv loading from a file in 5d24fd474d6e6bd065039d058ef64c6db4454df9. --- dist/recline.js | 2 +- src/backend.csv.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index 70093f41..e957a078 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -33,7 +33,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var reader = new FileReader(); var encoding = dataset.encoding || 'UTF-8'; reader.onload = function(e) { - var out = my.extractFields(my.parseCSV(data, dataset), dataset); + var out = my.extractFields(my.parseCSV(e.target.result, dataset), dataset); out.useMemoryStore = true; out.metadata = { filename: dataset.file.name diff --git a/src/backend.csv.js b/src/backend.csv.js index e5a8a599..1f868ffe 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -33,7 +33,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; var reader = new FileReader(); var encoding = dataset.encoding || 'UTF-8'; reader.onload = function(e) { - var out = my.extractFields(my.parseCSV(data, dataset), dataset); + var out = my.extractFields(my.parseCSV(e.target.result, dataset), dataset); out.useMemoryStore = true; out.metadata = { filename: dataset.file.name From 35b73aea5289759cc6b6b38aba5ae963ce7eb75f Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 30 Jun 2013 14:16:22 +0100 Subject: [PATCH 45/74] [be/csv,bugfix][xs]: fix to breakage of csv loading for urls in 5d24fd474d6e6bd065039d058ef64c6db4454df9. --- src/backend.csv.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend.csv.js b/src/backend.csv.js index 1f868ffe..6d805827 100644 --- a/src/backend.csv.js +++ b/src/backend.csv.js @@ -52,6 +52,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; jQuery.get(dataset.url).done(function(data) { var out = my.extractFields(my.parseCSV(data, dataset), dataset); out.useMemoryStore = true; + dfd.resolve(out); }); } return dfd.promise(); From f1ec8f047479607bf07b6f2f05dfa77f679622ed Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 30 Jun 2013 14:16:56 +0100 Subject: [PATCH 46/74] [build][xs]: . --- dist/recline.js | 1 + 1 file changed, 1 insertion(+) diff --git a/dist/recline.js b/dist/recline.js index e957a078..2577c701 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -52,6 +52,7 @@ this.recline.Backend.CSV = this.recline.Backend.CSV || {}; jQuery.get(dataset.url).done(function(data) { var out = my.extractFields(my.parseCSV(data, dataset), dataset); out.useMemoryStore = true; + dfd.resolve(out); }); } return dfd.promise(); From e4adc0c34ad176fc6ff022f5f47637b89f699949 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 30 Jun 2013 21:51:39 +0100 Subject: [PATCH 47/74] [#314,backends][m]: remove elasticsearch backend - now in own repo https://github.com/okfn/elasticsearch.js. --- _includes/backend-list.html | 2 +- _includes/example-backends-elasticsearch.js | 13 - _includes/recline-deps.html | 1 - dist/recline.js | 286 ---------------- docs/tutorial-backends.markdown | 17 +- src/backend.elasticsearch.js | 286 ---------------- test/backend.elasticsearch.test.js | 351 -------------------- test/index.html | 2 - 8 files changed, 5 insertions(+), 953 deletions(-) delete mode 100644 _includes/example-backends-elasticsearch.js delete mode 100644 src/backend.elasticsearch.js delete mode 100644 test/backend.elasticsearch.test.js diff --git a/_includes/backend-list.html b/_includes/backend-list.html index 88e5f38d..315f856f 100644 --- a/_includes/backend-list.html +++ b/_includes/backend-list.html @@ -2,7 +2,7 @@
  • gdocs: Google Docs (Spreadsheet)
  • csv: CSV files
  • solr: SOLR (partial)
  • -
  • elasticsearch: ElasticSearch
  • +
  • elasticsearch: ElasticSearch
  • dataproxy: DataProxy (CSV and XLS on the Web)
  • ckan: CKAN – support for CKAN datastore
  • couchdb: CouchDB
  • diff --git a/_includes/example-backends-elasticsearch.js b/_includes/example-backends-elasticsearch.js deleted file mode 100644 index 51d41c0c..00000000 --- a/_includes/example-backends-elasticsearch.js +++ /dev/null @@ -1,13 +0,0 @@ -var dataset = new recline.Model.Dataset({ - url: 'http://datahub.io/dataset/rendition-on-record/ac5a28ea-eb52-4b0a-a399-5dcc1becf9d9/api', - backend: 'elasticsearch' -}); - -dataset.fetch(); - -// For demonstrations purposes display the data in a grid -var grid = new recline.View.SlickGrid({ - model: dataset -}); -$('#my-elasticsearch').append(grid.el); - diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index cd46ce55..dbb4a23e 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -54,7 +54,6 @@ - diff --git a/dist/recline.js b/dist/recline.js index 2577c701..a163a421 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -381,292 +381,6 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; }; }(this.recline.Backend.DataProxy)); -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; - -(function($, my) { - "use strict"; - my.__type__ = 'elasticsearch'; - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## ElasticSearch Wrapper - // - // A simple JS wrapper around an [ElasticSearch](http://www.elasticsearch.org/) endpoints. - // - // @param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running - // on http://localhost:9200 with index twitter and type tweet it would be: - // - //
    http://localhost:9200/twitter/tweet
    - // - // @param {Object} options: set of options such as: - // - // * headers - {dict of headers to add to each request} - // * dataType: dataType for AJAx requests e.g. set to jsonp to make jsonp requests (default is json requests) - my.Wrapper = function(endpoint, options) { - var self = this; - this.endpoint = endpoint; - this.options = _.extend({ - dataType: 'json' - }, - options); - - // ### mapping - // - // Get ES mapping for this type/table - // - // @return promise compatible deferred object. - this.mapping = function() { - var schemaUrl = self.endpoint + '/_mapping'; - var jqxhr = makeRequest({ - url: schemaUrl, - dataType: this.options.dataType - }); - return jqxhr; - }; - - // ### get - // - // Get record corresponding to specified id - // - // @return promise compatible deferred object. - this.get = function(id) { - var base = this.endpoint + '/' + id; - return makeRequest({ - url: base, - dataType: 'json' - }); - }; - - // ### upsert - // - // create / update a record to ElasticSearch backend - // - // @param {Object} doc an object to insert to the index. - // @return deferred supporting promise API - this.upsert = function(doc) { - var data = JSON.stringify(doc); - url = this.endpoint; - if (doc.id) { - url += '/' + doc.id; - } - return makeRequest({ - url: url, - type: 'POST', - data: data, - dataType: 'json' - }); - }; - - // ### delete - // - // Delete a record from the ElasticSearch backend. - // - // @param {Object} id id of object to delete - // @return deferred supporting promise API - this.remove = function(id) { - url = this.endpoint; - url += '/' + id; - return makeRequest({ - url: url, - type: 'DELETE', - dataType: 'json' - }); - }; - - this._normalizeQuery = function(queryObj) { - var self = this; - var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj); - var out = { - constant_score: { - query: {} - } - }; - if (!queryInfo.q) { - out.constant_score.query = { - match_all: {} - }; - } else { - out.constant_score.query = { - query_string: { - query: queryInfo.q - } - }; - } - if (queryInfo.filters && queryInfo.filters.length) { - out.constant_score.filter = { - and: [] - }; - _.each(queryInfo.filters, function(filter) { - out.constant_score.filter.and.push(self._convertFilter(filter)); - }); - } - return out; - }, - - // convert from Recline sort structure to ES form - // http://www.elasticsearch.org/guide/reference/api/search/sort.html - this._normalizeSort = function(sort) { - var out = _.map(sort, function(sortObj) { - var _tmp = {}; - var _tmp2 = _.clone(sortObj); - delete _tmp2['field']; - _tmp[sortObj.field] = _tmp2; - return _tmp; - }); - return out; - }, - - this._convertFilter = function(filter) { - var out = {}; - out[filter.type] = {}; - if (filter.type === 'term') { - out.term[filter.field] = filter.term.toLowerCase(); - } else if (filter.type === 'geo_distance') { - out.geo_distance[filter.field] = filter.point; - out.geo_distance.distance = filter.distance; - out.geo_distance.unit = filter.unit; - } - return out; - }, - - // ### query - // - // @return deferred supporting promise API - this.query = function(queryObj) { - var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj); - esQuery.query = this._normalizeQuery(queryObj); - delete esQuery.q; - delete esQuery.filters; - if (esQuery.sort && esQuery.sort.length > 0) { - esQuery.sort = this._normalizeSort(esQuery.sort); - } - var data = {source: JSON.stringify(esQuery)}; - var url = this.endpoint + '/_search'; - var jqxhr = makeRequest({ - url: url, - data: data, - dataType: this.options.dataType - }); - return jqxhr; - }; - }; - - - // ## Recline Connectors - // - // Requires URL of ElasticSearch endpoint to be specified on the dataset - // via the url attribute. - - // ES options which are passed through to `options` on Wrapper (see Wrapper for details) - my.esOptions = {}; - - // ### fetch - my.fetch = function(dataset) { - var es = new my.Wrapper(dataset.url, my.esOptions); - var dfd = new Deferred(); - es.mapping().done(function(schema) { - - if (!schema){ - dfd.reject({'message':'Elastic Search did not return a mapping'}); - return; - } - - // only one top level key in ES = the type so we can ignore it - var key = _.keys(schema)[0]; - var fieldData = _.map(schema[key].properties, function(dict, fieldName) { - dict.id = fieldName; - return dict; - }); - dfd.resolve({ - fields: fieldData - }); - }) - .fail(function(args) { - dfd.reject(args); - }); - return dfd.promise(); - }; - - // ### save - my.save = function(changes, dataset) { - var es = new my.Wrapper(dataset.url, my.esOptions); - if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) { - var dfd = new Deferred(); - msg = 'Saving more than one item at a time not yet supported'; - alert(msg); - dfd.reject(msg); - return dfd.promise(); - } - if (changes.creates.length > 0) { - return es.upsert(changes.creates[0]); - } - else if (changes.updates.length >0) { - return es.upsert(changes.updates[0]); - } else if (changes.deletes.length > 0) { - return es.remove(changes.deletes[0].id); - } - }; - - // ### query - my.query = function(queryObj, dataset) { - var dfd = new Deferred(); - var es = new my.Wrapper(dataset.url, my.esOptions); - var jqxhr = es.query(queryObj); - jqxhr.done(function(results) { - var out = { - total: results.hits.total - }; - out.hits = _.map(results.hits.hits, function(hit) { - if (!('id' in hit._source) && hit._id) { - hit._source.id = hit._id; - } - return hit._source; - }); - if (results.facets) { - out.facets = results.facets; - } - dfd.resolve(out); - }).fail(function(errorObj) { - var out = { - title: 'Failed: ' + errorObj.status + ' code', - message: errorObj.responseText - }; - dfd.reject(out); - }); - return dfd.promise(); - }; - - -// ### makeRequest -// -// Just $.ajax but in any headers in the 'headers' attribute of this -// Backend instance. Example: -// -//
    -// var jqxhr = this._makeRequest({
    -//   url: the-url
    -// });
    -// 
    -var makeRequest = function(data, headers) { - var extras = {}; - if (headers) { - extras = { - beforeSend: function(req) { - _.each(headers, function(value, key) { - req.setRequestHeader(key, value); - }); - } - }; - } - var data = _.extend(extras, data); - return $.ajax(data); -}; - -}(jQuery, this.recline.Backend.ElasticSearch)); - this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; diff --git a/docs/tutorial-backends.markdown b/docs/tutorial-backends.markdown index 2f8c3594..032452ba 100644 --- a/docs/tutorial-backends.markdown +++ b/docs/tutorial-backends.markdown @@ -111,21 +111,12 @@ a bespoke chooser and a Kartograph (svg-only) map. -## Loading Data from ElasticSearch and the DataHub +## Loading Data from ElasticSearch -Recline supports ElasticSearch as a full read/write/query backend. It also means that Recline can load data from the [DataHub's](http://datahub.io/) data API as that is ElasticSearch compatible. Here's an example, using [this dataset about Rendition flights](http://datahub.io/dataset/rendition-on-record/ac5a28ea-eb52-4b0a-a399-5dcc1becf9d9') on the DataHub: +Recline supports ElasticSearch as a full read/write/query backend via the +[ElasticSearch.js library][esjs]. See the library for examples. -{% highlight javascript %} -{% include example-backends-elasticsearch.js %} -{% endhighlight %} - -### Result - -
     
    - - +[esjs]: https://github.com/okfn/elasticsearch.js ## Loading data from CSV files diff --git a/src/backend.elasticsearch.js b/src/backend.elasticsearch.js deleted file mode 100644 index 7c32c9ad..00000000 --- a/src/backend.elasticsearch.js +++ /dev/null @@ -1,286 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.ElasticSearch = this.recline.Backend.ElasticSearch || {}; - -(function($, my) { - "use strict"; - my.__type__ = 'elasticsearch'; - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## ElasticSearch Wrapper - // - // A simple JS wrapper around an [ElasticSearch](http://www.elasticsearch.org/) endpoints. - // - // @param {String} endpoint: url for ElasticSearch type/table, e.g. for ES running - // on http://localhost:9200 with index twitter and type tweet it would be: - // - //
    http://localhost:9200/twitter/tweet
    - // - // @param {Object} options: set of options such as: - // - // * headers - {dict of headers to add to each request} - // * dataType: dataType for AJAx requests e.g. set to jsonp to make jsonp requests (default is json requests) - my.Wrapper = function(endpoint, options) { - var self = this; - this.endpoint = endpoint; - this.options = _.extend({ - dataType: 'json' - }, - options); - - // ### mapping - // - // Get ES mapping for this type/table - // - // @return promise compatible deferred object. - this.mapping = function() { - var schemaUrl = self.endpoint + '/_mapping'; - var jqxhr = makeRequest({ - url: schemaUrl, - dataType: this.options.dataType - }); - return jqxhr; - }; - - // ### get - // - // Get record corresponding to specified id - // - // @return promise compatible deferred object. - this.get = function(id) { - var base = this.endpoint + '/' + id; - return makeRequest({ - url: base, - dataType: 'json' - }); - }; - - // ### upsert - // - // create / update a record to ElasticSearch backend - // - // @param {Object} doc an object to insert to the index. - // @return deferred supporting promise API - this.upsert = function(doc) { - var data = JSON.stringify(doc); - url = this.endpoint; - if (doc.id) { - url += '/' + doc.id; - } - return makeRequest({ - url: url, - type: 'POST', - data: data, - dataType: 'json' - }); - }; - - // ### delete - // - // Delete a record from the ElasticSearch backend. - // - // @param {Object} id id of object to delete - // @return deferred supporting promise API - this.remove = function(id) { - url = this.endpoint; - url += '/' + id; - return makeRequest({ - url: url, - type: 'DELETE', - dataType: 'json' - }); - }; - - this._normalizeQuery = function(queryObj) { - var self = this; - var queryInfo = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj); - var out = { - constant_score: { - query: {} - } - }; - if (!queryInfo.q) { - out.constant_score.query = { - match_all: {} - }; - } else { - out.constant_score.query = { - query_string: { - query: queryInfo.q - } - }; - } - if (queryInfo.filters && queryInfo.filters.length) { - out.constant_score.filter = { - and: [] - }; - _.each(queryInfo.filters, function(filter) { - out.constant_score.filter.and.push(self._convertFilter(filter)); - }); - } - return out; - }, - - // convert from Recline sort structure to ES form - // http://www.elasticsearch.org/guide/reference/api/search/sort.html - this._normalizeSort = function(sort) { - var out = _.map(sort, function(sortObj) { - var _tmp = {}; - var _tmp2 = _.clone(sortObj); - delete _tmp2['field']; - _tmp[sortObj.field] = _tmp2; - return _tmp; - }); - return out; - }, - - this._convertFilter = function(filter) { - var out = {}; - out[filter.type] = {}; - if (filter.type === 'term') { - out.term[filter.field] = filter.term.toLowerCase(); - } else if (filter.type === 'geo_distance') { - out.geo_distance[filter.field] = filter.point; - out.geo_distance.distance = filter.distance; - out.geo_distance.unit = filter.unit; - } - return out; - }, - - // ### query - // - // @return deferred supporting promise API - this.query = function(queryObj) { - var esQuery = (queryObj && queryObj.toJSON) ? queryObj.toJSON() : _.extend({}, queryObj); - esQuery.query = this._normalizeQuery(queryObj); - delete esQuery.q; - delete esQuery.filters; - if (esQuery.sort && esQuery.sort.length > 0) { - esQuery.sort = this._normalizeSort(esQuery.sort); - } - var data = {source: JSON.stringify(esQuery)}; - var url = this.endpoint + '/_search'; - var jqxhr = makeRequest({ - url: url, - data: data, - dataType: this.options.dataType - }); - return jqxhr; - }; - }; - - - // ## Recline Connectors - // - // Requires URL of ElasticSearch endpoint to be specified on the dataset - // via the url attribute. - - // ES options which are passed through to `options` on Wrapper (see Wrapper for details) - my.esOptions = {}; - - // ### fetch - my.fetch = function(dataset) { - var es = new my.Wrapper(dataset.url, my.esOptions); - var dfd = new Deferred(); - es.mapping().done(function(schema) { - - if (!schema){ - dfd.reject({'message':'Elastic Search did not return a mapping'}); - return; - } - - // only one top level key in ES = the type so we can ignore it - var key = _.keys(schema)[0]; - var fieldData = _.map(schema[key].properties, function(dict, fieldName) { - dict.id = fieldName; - return dict; - }); - dfd.resolve({ - fields: fieldData - }); - }) - .fail(function(args) { - dfd.reject(args); - }); - return dfd.promise(); - }; - - // ### save - my.save = function(changes, dataset) { - var es = new my.Wrapper(dataset.url, my.esOptions); - if (changes.creates.length + changes.updates.length + changes.deletes.length > 1) { - var dfd = new Deferred(); - msg = 'Saving more than one item at a time not yet supported'; - alert(msg); - dfd.reject(msg); - return dfd.promise(); - } - if (changes.creates.length > 0) { - return es.upsert(changes.creates[0]); - } - else if (changes.updates.length >0) { - return es.upsert(changes.updates[0]); - } else if (changes.deletes.length > 0) { - return es.remove(changes.deletes[0].id); - } - }; - - // ### query - my.query = function(queryObj, dataset) { - var dfd = new Deferred(); - var es = new my.Wrapper(dataset.url, my.esOptions); - var jqxhr = es.query(queryObj); - jqxhr.done(function(results) { - var out = { - total: results.hits.total - }; - out.hits = _.map(results.hits.hits, function(hit) { - if (!('id' in hit._source) && hit._id) { - hit._source.id = hit._id; - } - return hit._source; - }); - if (results.facets) { - out.facets = results.facets; - } - dfd.resolve(out); - }).fail(function(errorObj) { - var out = { - title: 'Failed: ' + errorObj.status + ' code', - message: errorObj.responseText - }; - dfd.reject(out); - }); - return dfd.promise(); - }; - - -// ### makeRequest -// -// Just $.ajax but in any headers in the 'headers' attribute of this -// Backend instance. Example: -// -//
    -// var jqxhr = this._makeRequest({
    -//   url: the-url
    -// });
    -// 
    -var makeRequest = function(data, headers) { - var extras = {}; - if (headers) { - extras = { - beforeSend: function(req) { - _.each(headers, function(value, key) { - req.setRequestHeader(key, value); - }); - } - }; - } - var data = _.extend(extras, data); - return $.ajax(data); -}; - -}(jQuery, this.recline.Backend.ElasticSearch)); - diff --git a/test/backend.elasticsearch.test.js b/test/backend.elasticsearch.test.js deleted file mode 100644 index c97bc744..00000000 --- a/test/backend.elasticsearch.test.js +++ /dev/null @@ -1,351 +0,0 @@ -(function ($) { -module("Backend ElasticSearch - Wrapper"); - -test("queryNormalize", function() { - var backend = new recline.Backend.ElasticSearch.Wrapper(); - - var in_ = new recline.Model.Query(); - var out = backend._normalizeQuery(in_); - var exp = { - constant_score: { - query: { - match_all: {} - } - } - }; - deepEqual(out, exp); - - var in_ = new recline.Model.Query(); - in_.set({q: ''}); - var out = backend._normalizeQuery(in_); - deepEqual(out, exp); - - var in_ = new recline.Model.Query(); - in_.attributes.q = 'abc'; - var out = backend._normalizeQuery(in_); - equal(out.constant_score.query.query_string.query, 'abc'); - - var in_ = new recline.Model.Query(); - in_.addFilter({ - type: 'term', - field: 'xyz', - term: 'XXX' - }); - var out = backend._normalizeQuery(in_); - var exp = { - constant_score: { - query: { - match_all: {} - }, - filter: { - and: [ - { - term: { - xyz: 'xxx' - } - } - ] - } - } - }; - deepEqual(out, exp); - - var in_ = new recline.Model.Query(); - in_.addFilter({ - type: 'geo_distance', - field: 'xyz' - }); - var out = backend._normalizeQuery(in_); - var exp = { - constant_score: { - query: { - match_all: {} - }, - filter: { - and: [ - { - geo_distance: { - distance: 10, - unit: 'km', - 'xyz': { lon: 0, lat: 0 } - } - } - ] - } - } - }; - deepEqual(out, exp); -}); - -var mapping_data = { - "note": { - "properties": { - "_created": { - "format": "dateOptionalTime", - "type": "date" - }, - "_last_modified": { - "format": "dateOptionalTime", - "type": "date" - }, - "end": { - "type": "string" - }, - "owner": { - "type": "string" - }, - "start": { - "type": "string" - }, - "title": { - "type": "string" - } - } - } -}; - -var sample_data = { - "_shards": { - "failed": 0, - "successful": 5, - "total": 5 - }, - "hits": { - "hits": [ - { - "_id": "u3rpLyuFS3yLNXrtxWkMwg", - "_index": "hypernotes", - "_score": 1.0, - "_source": { - "_created": "2012-02-24T17:53:57.286Z", - "_last_modified": "2012-02-24T17:53:57.286Z", - "owner": "tester", - "title": "Note 1" - }, - "_type": "note" - }, - { - "_id": "n7JMkFOHSASJCVTXgcpqkA", - "_index": "hypernotes", - "_score": 1.0, - "_source": { - "_created": "2012-02-24T17:53:57.290Z", - "_last_modified": "2012-02-24T17:53:57.290Z", - "owner": "tester", - "title": "Note 3" - }, - "_type": "note" - }, - { - "_id": "g7UMA55gTJijvsB3dFitzw", - "_index": "hypernotes", - "_score": 1.0, - "_source": { - "_created": "2012-02-24T17:53:57.289Z", - "_last_modified": "2012-02-24T17:53:57.289Z", - "owner": "tester", - "title": "Note 2" - }, - "_type": "note" - } - ], - "max_score": 1.0, - "total": 3 - }, - "timed_out": false, - "took": 2 -}; - -test("query", function() { - var backend = new recline.Backend.ElasticSearch.Wrapper('https://localhost:9200/my-es-db/my-es-type'); - - var stub = sinon.stub($, 'ajax', function(options) { - if (options.url.indexOf('_mapping') != -1) { - return { - done: function(callback) { - callback(mapping_data); - return this; - }, - fail: function() { - return this; - } - } - } else { - return { - done: function(callback) { - callback(sample_data); - return this; - }, - fail: function() { - } - } - } - }); - - backend.mapping().done(function(data) { - var fields = _.keys(data.note.properties); - deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], fields); - }); - - backend.query().done(function(queryResult) { - equal(3, queryResult.hits.total); - equal(3, queryResult.hits.hits.length); - equal('Note 1', queryResult.hits.hits[0]._source['title']); - }); - $.ajax.restore(); -}); - -// DISABLED - this test requires ElasticSearch to be running locally -// test("write", function() { -// var url = 'http://localhost:9200/recline-test/es-write'; -// var backend = new recline.Backend.ElasticSearch.Wrapper(url); -// stop(); -// -// var id = parseInt(Math.random()*100000000).toString(); -// var rec = { -// id: id, -// title: 'my title' -// }; -// var jqxhr = backend.upsert(rec); -// jqxhr.done(function(data) { -// ok(data.ok); -// equal(data._id, id); -// equal(data._type, 'es-write'); -// equal(data._version, 1); -// -// // update -// rec.title = 'new title'; -// var jqxhr = backend.upsert(rec); -// jqxhr.done(function(data) { -// equal(data._version, 2); -// -// // delete -// var jqxhr = backend.remove(rec.id); -// jqxhr.done(function(data) { -// ok(data.ok); -// rec = null; -// -// // try to get ... -// var jqxhr = backend.get(id); -// jqxhr.done(function(data) { -// // should not be here -// ok(false, 'Should have got 404'); -// }).error(function(error) { -// equal(error.status, 404); -// start(); -// }); -// }); -// }); -// }).fail(function(error) { -// console.log(error); -// ok(false, 'Basic request failed - is ElasticSearch running locally on port 9200 (required for this test!)'); -// start(); -// }); -// }); - - -// ================================================== - -module("Backend ElasticSearch - Recline"); - -test("query", function() { - var dataset = new recline.Model.Dataset({ - url: 'https://localhost:9200/my-es-db/my-es-type', - backend: 'elasticsearch' - }); - - var stub = sinon.stub($, 'ajax', function(options) { - if (options.url.indexOf('_mapping') != -1) { - return { - done: function(callback) { - callback(mapping_data); - return this; - }, - fail: function() { - return this; - } - } - } else { - return { - done: function(callback) { - callback(sample_data); - return this; - }, - fail: function() { - } - }; - } - }); - - dataset.fetch().done(function(dataset) { - deepEqual(['_created', '_last_modified', 'end', 'owner', 'start', 'title'], _.pluck(dataset.fields.toJSON(), 'id')); - dataset.query().then(function(recList) { - equal(3, dataset.recordCount); - equal(3, recList.length); - equal('Note 1', recList.models[0].get('title')); - }); - }); - $.ajax.restore(); -}); - -// DISABLED - this test requires ElasticSearch to be running locally -// test("write", function() { -// var dataset = new recline.Model.Dataset({ -// url: 'http://localhost:9200/recline-test/es-write', -// backend: 'elasticsearch' -// }); -// -// stop(); -// -// var id = parseInt(Math.random()*100000000).toString(); -// var rec = new recline.Model.Record({ -// id: id, -// title: 'my title' -// }); -// dataset.records.add(rec); -// // have to do this explicitly as we not really supporting adding new items atm -// dataset._changes.creates.push(rec.toJSON()); -// var jqxhr = dataset.save(); -// jqxhr.done(function(data) { -// ok(data.ok); -// equal(data._id, id); -// equal(data._type, 'es-write'); -// equal(data._version, 1); -// -// // update -// rec.set({title: 'new title'}); -// // again set up by hand ... -// dataset._changes.creates = []; -// dataset._changes.updates.push(rec.toJSON()); -// var jqxhr = dataset.save(); -// jqxhr.done(function(data) { -// equal(data._version, 2); -// -// // delete -// dataset._changes.updates = 0; -// dataset._changes.deletes.push(rec.toJSON()); -// var jqxhr = dataset.save(); -// jqxhr.done(function(data) { -// ok(data.ok); -// rec = null; -// -// // try to get ... -// var es = new recline.Backend.ElasticSearch.Wrapper(dataset.get('url')); -// var jqxhr = es.get(id); -// jqxhr.done(function(data) { -// // should not be here -// ok(false, 'Should have got 404'); -// }).error(function(error) { -// equal(error.status, 404); -// start(); -// }); -// }); -// }); -// }).fail(function(error) { -// console.log(error); -// ok(false, 'Basic request failed - is ElasticSearch running locally on port 9200 (required for this test!)'); -// start(); -// }); -// }); - -})(this.jQuery); diff --git a/test/index.html b/test/index.html index ddb4aa92..1c103cac 100644 --- a/test/index.html +++ b/test/index.html @@ -41,14 +41,12 @@ - - From d1b04a4d1009c110f114096c55888ee4dcfb9ef9 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 30 Jun 2013 22:08:06 +0100 Subject: [PATCH 48/74] [#314,backend][s]: remove gdocs backend - now in separate repo. --- README.md | 1 + _includes/recline-deps.html | 2 +- dist/recline.js | 168 ----------------- docs/tutorial-backends.markdown | 8 +- src/backend.gdocs.js | 168 ----------------- test/backend.gdocs.test.js | 320 -------------------------------- test/index.html | 2 - 7 files changed, 8 insertions(+), 661 deletions(-) delete mode 100644 src/backend.gdocs.js delete mode 100644 test/backend.gdocs.test.js diff --git a/README.md b/README.md index a7fc7367..f4f582b0 100755 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ See CONTRIBUTING.md. Possible breaking changes +* Many backends moved to their own repositories #314 * Updated Leaflet to latest version 0.4.4 #220 * Added marker clustering in map view to handle a large number of markers * Dataset.restore method removed (not used internally except from Multiview.restore) diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index dbb4a23e..6055809a 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -53,7 +53,7 @@ - + diff --git a/dist/recline.js b/dist/recline.js index a163a421..41168e0c 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -383,174 +383,6 @@ this.recline.Backend.DataProxy = this.recline.Backend.DataProxy || {}; }(this.recline.Backend.DataProxy)); this.recline = this.recline || {}; this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; - -(function(my) { - "use strict"; - my.__type__ = 'gdocs'; - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## Google spreadsheet backend - // - // Fetch data from a Google Docs spreadsheet. - // - // Dataset must have a url attribute pointing to the Gdocs or its JSON feed e.g. - //
    -  // var dataset = new recline.Model.Dataset({
    -  //     url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGlQVDJnbjZRSU1tUUJWOUZXRG53VkE#gid=0'
    -  //   },
    -  //   'gdocs'
    -  // );
    -  //
    -  // var dataset = new recline.Model.Dataset({
    -  //     url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
    -  //   },
    -  //   'gdocs'
    -  // );
    -  // 
    - // - // @return object with two attributes - // - // * fields: array of Field objects - // * records: array of objects for each row - my.fetch = function(dataset) { - var dfd = new Deferred(); - var urls = my.getGDocsAPIUrls(dataset.url); - - // TODO cover it with tests - // get the spreadsheet title - (function () { - var titleDfd = new Deferred(); - - jQuery.getJSON(urls.spreadsheet, function (d) { - titleDfd.resolve({ - spreadsheetTitle: d.feed.title.$t - }); - }); - - return titleDfd.promise(); - }()).then(function (response) { - - // get the actual worksheet data - jQuery.getJSON(urls.worksheet, function(d) { - var result = my.parseData(d); - var fields = _.map(result.fields, function(fieldId) { - return {id: fieldId}; - }); - - dfd.resolve({ - metadata: { - title: response.spreadsheetTitle +" :: "+ result.worksheetTitle, - spreadsheetTitle: response.spreadsheetTitle, - worksheetTitle : result.worksheetTitle - }, - records : result.records, - fields : fields, - useMemoryStore: true - }); - }); - }); - - return dfd.promise(); - }; - - // ## parseData - // - // Parse data from Google Docs API into a reasonable form - // - // :options: (optional) optional argument dictionary: - // columnsToUse: list of columns to use (specified by field names) - // colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). - // :return: tabular data object (hash with keys: field and data). - // - // Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. - my.parseData = function(gdocsSpreadsheet, options) { - var options = options || {}; - var colTypes = options.colTypes || {}; - var results = { - fields : [], - records: [] - }; - var entries = gdocsSpreadsheet.feed.entry || []; - var key; - var colName; - // percentage values (e.g. 23.3%) - var rep = /^([\d\.\-]+)\%$/; - - for(key in entries[0]) { - // it's barely possible it has inherited keys starting with 'gsx$' - if(/^gsx/.test(key)) { - colName = key.substr(4); - results.fields.push(colName); - } - } - - // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) - results.records = _.map(entries, function(entry) { - var row = {}; - - _.each(results.fields, function(col) { - var _keyname = 'gsx$' + col; - var value = entry[_keyname].$t; - var num; - - // TODO cover this part of code with test - // TODO use the regexp only once - // if labelled as % and value contains %, convert - if(colTypes[col] === 'percent' && rep.test(value)) { - num = rep.exec(value)[1]; - value = parseFloat(num) / 100; - } - - row[col] = value; - }); - - return row; - }); - - results.worksheetTitle = gdocsSpreadsheet.feed.title.$t; - return results; - }; - - // Convenience function to get GDocs JSON API Url from standard URL - my.getGDocsAPIUrls = function(url) { - // https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY - var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+)[^#]*(#gid=([\d]+).*)?/; - var matches = url.match(regex); - var key; - var worksheet; - var urls; - - if(!!matches) { - key = matches[1]; - // the gid in url is 0-based and feed url is 1-based - worksheet = parseInt(matches[3], 10) + 1; - if (isNaN(worksheet)) { - worksheet = 1; - } - urls = { - worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', - spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - }; - } - else { - // we assume that it's one of the feeds urls - key = url.split('/')[5]; - // by default then, take first worksheet - worksheet = 1; - urls = { - worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', - spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - }; - } - - return urls; - }; -}(this.recline.Backend.GDocs)); -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; this.recline.Backend.Memory = this.recline.Backend.Memory || {}; (function(my) { diff --git a/docs/tutorial-backends.markdown b/docs/tutorial-backends.markdown index 032452ba..1b27fb0a 100644 --- a/docs/tutorial-backends.markdown +++ b/docs/tutorial-backends.markdown @@ -76,8 +76,8 @@ much more limited if you are just using a Backend. Specifically: - - + + @@ -99,6 +99,8 @@ a bespoke chooser and a Kartograph (svg-only) map. {% highlight javascript %} +// include the Recline backend for Google Docs + {% include example-backends-gdocs.js %} {% endhighlight %} @@ -106,6 +108,8 @@ a bespoke chooser and a Kartograph (svg-only) map.
     
    + + diff --git a/src/backend.gdocs.js b/src/backend.gdocs.js deleted file mode 100644 index 16976884..00000000 --- a/src/backend.gdocs.js +++ /dev/null @@ -1,168 +0,0 @@ -this.recline = this.recline || {}; -this.recline.Backend = this.recline.Backend || {}; -this.recline.Backend.GDocs = this.recline.Backend.GDocs || {}; - -(function(my) { - "use strict"; - my.__type__ = 'gdocs'; - - // use either jQuery or Underscore Deferred depending on what is available - var Deferred = (typeof jQuery !== "undefined" && jQuery.Deferred) || _.Deferred; - - // ## Google spreadsheet backend - // - // Fetch data from a Google Docs spreadsheet. - // - // Dataset must have a url attribute pointing to the Gdocs or its JSON feed e.g. - //
    -  // var dataset = new recline.Model.Dataset({
    -  //     url: 'https://docs.google.com/spreadsheet/ccc?key=0Aon3JiuouxLUdGlQVDJnbjZRSU1tUUJWOUZXRG53VkE#gid=0'
    -  //   },
    -  //   'gdocs'
    -  // );
    -  //
    -  // var dataset = new recline.Model.Dataset({
    -  //     url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json'
    -  //   },
    -  //   'gdocs'
    -  // );
    -  // 
    - // - // @return object with two attributes - // - // * fields: array of Field objects - // * records: array of objects for each row - my.fetch = function(dataset) { - var dfd = new Deferred(); - var urls = my.getGDocsAPIUrls(dataset.url); - - // TODO cover it with tests - // get the spreadsheet title - (function () { - var titleDfd = new Deferred(); - - jQuery.getJSON(urls.spreadsheet, function (d) { - titleDfd.resolve({ - spreadsheetTitle: d.feed.title.$t - }); - }); - - return titleDfd.promise(); - }()).then(function (response) { - - // get the actual worksheet data - jQuery.getJSON(urls.worksheet, function(d) { - var result = my.parseData(d); - var fields = _.map(result.fields, function(fieldId) { - return {id: fieldId}; - }); - - dfd.resolve({ - metadata: { - title: response.spreadsheetTitle +" :: "+ result.worksheetTitle, - spreadsheetTitle: response.spreadsheetTitle, - worksheetTitle : result.worksheetTitle - }, - records : result.records, - fields : fields, - useMemoryStore: true - }); - }); - }); - - return dfd.promise(); - }; - - // ## parseData - // - // Parse data from Google Docs API into a reasonable form - // - // :options: (optional) optional argument dictionary: - // columnsToUse: list of columns to use (specified by field names) - // colTypes: dictionary (with column names as keys) specifying types (e.g. range, percent for use in conversion). - // :return: tabular data object (hash with keys: field and data). - // - // Issues: seems google docs return columns in rows in random order and not even sure whether consistent across rows. - my.parseData = function(gdocsSpreadsheet, options) { - var options = options || {}; - var colTypes = options.colTypes || {}; - var results = { - fields : [], - records: [] - }; - var entries = gdocsSpreadsheet.feed.entry || []; - var key; - var colName; - // percentage values (e.g. 23.3%) - var rep = /^([\d\.\-]+)\%$/; - - for(key in entries[0]) { - // it's barely possible it has inherited keys starting with 'gsx$' - if(/^gsx/.test(key)) { - colName = key.substr(4); - results.fields.push(colName); - } - } - - // converts non numberical values that should be numerical (22.3%[string] -> 0.223[float]) - results.records = _.map(entries, function(entry) { - var row = {}; - - _.each(results.fields, function(col) { - var _keyname = 'gsx$' + col; - var value = entry[_keyname].$t; - var num; - - // TODO cover this part of code with test - // TODO use the regexp only once - // if labelled as % and value contains %, convert - if(colTypes[col] === 'percent' && rep.test(value)) { - num = rep.exec(value)[1]; - value = parseFloat(num) / 100; - } - - row[col] = value; - }); - - return row; - }); - - results.worksheetTitle = gdocsSpreadsheet.feed.title.$t; - return results; - }; - - // Convenience function to get GDocs JSON API Url from standard URL - my.getGDocsAPIUrls = function(url) { - // https://docs.google.com/spreadsheet/ccc?key=XXXX#gid=YYY - var regex = /.*spreadsheet\/ccc?.*key=([^#?&+]+)[^#]*(#gid=([\d]+).*)?/; - var matches = url.match(regex); - var key; - var worksheet; - var urls; - - if(!!matches) { - key = matches[1]; - // the gid in url is 0-based and feed url is 1-based - worksheet = parseInt(matches[3], 10) + 1; - if (isNaN(worksheet)) { - worksheet = 1; - } - urls = { - worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', - spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - }; - } - else { - // we assume that it's one of the feeds urls - key = url.split('/')[5]; - // by default then, take first worksheet - worksheet = 1; - urls = { - worksheet : 'https://spreadsheets.google.com/feeds/list/'+ key +'/'+ worksheet +'/public/values?alt=json', - spreadsheet: 'https://spreadsheets.google.com/feeds/worksheets/'+ key +'/public/basic?alt=json' - }; - } - - return urls; - }; -}(this.recline.Backend.GDocs)); diff --git a/test/backend.gdocs.test.js b/test/backend.gdocs.test.js deleted file mode 100644 index 0e860b9c..00000000 --- a/test/backend.gdocs.test.js +++ /dev/null @@ -1,320 +0,0 @@ -(function ($) { -module("Backend GDocs"); - -var sampleGDocsSpreadsheetMetadata = { - feed: { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#worksheet", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-13T09:57:28.408Z" - }, - xmlns: "http://www.w3.org/2005/Atom", - title: { - $t: "javascript-test", - type: "text" - }, - author: [ - { - name: { - $t: "okfn.rufus.pollock" - }, - email: { - $t: "okfn.rufus.pollock@gmail.com" - } - } - ], - openSearch$startIndex: { - $t: "1" - }, - xmlns$gs: "http://schemas.google.com/spreadsheets/2006", - xmlns$openSearch: "http://a9.com/-/spec/opensearchrss/1.0/", - entry: [ - { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#worksheet", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-13T09:57:28.408Z" - }, - title: { - $t: "Sheet1", - type: "text" - }, - content: { - $t: "Sheet1", - type: "text" - }, - link: [ - { - href: "https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/basic", - type: "application/atom+xml", - rel: "http://schemas.google.com/spreadsheets/2006#listfeed" - }, - { - href: "https://spreadsheets.google.com/feeds/cells/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/basic", - type: "application/atom+xml", - rel: "http://schemas.google.com/spreadsheets/2006#cellsfeed" - }, - { - href: "https://spreadsheets.google.com/tq?key=0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc&sheet=od6&pub=1", - type: "application/atom+xml", - rel: "http://schemas.google.com/visualization/2008#visualizationApi" - }, - { - href: "https://spreadsheets.google.com/feeds/worksheets/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/public/basic/od6", - type: "application/atom+xml", - rel: "self" - } - ], - id: { - $t: "https://spreadsheets.google.com/feeds/worksheets/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/public/basic/od6" - } - } - ], - link: [ - { - href: "https://spreadsheets.google.com/pub?key=0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc", - type: "text/html", - rel: "alternate" - }, - { - href: "https://spreadsheets.google.com/feeds/worksheets/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/public/basic", - type: "application/atom+xml", - rel: "http://schemas.google.com/g/2005#feed" - }, - { - href: "https://spreadsheets.google.com/feeds/worksheets/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/public/basic?alt=json", - type: "application/atom+xml", - rel: "self" - } - ], - openSearch$totalResults: { - $t: "1" - }, - id: { - $t: "https://spreadsheets.google.com/feeds/worksheets/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/public/basic" - } - }, - version: "1.0", - encoding: "UTF-8" -} - -var sampleGDocsSpreadsheetData = { - feed: { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#list", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-12T18:32:16.200Z" - }, - xmlns: "http://www.w3.org/2005/Atom", - xmlns$gsx: "http://schemas.google.com/spreadsheets/2006/extended", - title: { - $t: "Sheet1", - type: "text" - }, - author: [ - { - name: { - $t: "okfn.rufus.pollock" - }, - email: { - $t: "okfn.rufus.pollock@gmail.com" - } - } - ], - openSearch$startIndex: { - $t: "1" - }, - link: [ - { - href: "http://spreadsheets.google.com/pub?key=0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc", - type: "text/html", - rel: "alternate" - }, - { - href: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values", - type: "application/atom+xml", - rel: "http://schemas.google.com/g/2005#feed" - }, - { - href: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json-in-script", - type: "application/atom+xml", - rel: "self" - } - ], - xmlns$openSearch: "http://a9.com/-/spec/opensearchrss/1.0/", - entry: [ - { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#list", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-12T18:32:16.200Z" - }, - 'gsx$column-2': { - $t: "1" - }, - 'gsx$column-1': { - $t: "A" - }, - title: { - $t: "A", - type: "text" - }, - content: { - $t: "column-2: 1", - type: "text" - }, - link: [ - { - href: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cokwr", - type: "application/atom+xml", - rel: "self" - } - ], - id: { - $t: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cokwr" - } - }, - { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#list", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-12T18:32:16.200Z" - }, - 'gsx$column-2': { - $t: "2" - }, - 'gsx$column-1': { - $t: "b" - }, - title: { - $t: "b", - type: "text" - }, - content: { - $t: "column-2: 2", - type: "text" - }, - link: [ - { - href: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cpzh4", - type: "application/atom+xml", - rel: "self" - } - ], - id: { - $t: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cpzh4" - } - }, - { - category: [ - { - term: "http://schemas.google.com/spreadsheets/2006#list", - scheme: "http://schemas.google.com/spreadsheets/2006" - } - ], - updated: { - $t: "2010-07-12T18:32:16.200Z" - }, - 'gsx$column-2': { - $t: "3" - }, - 'gsx$column-1': { - $t: "c" - }, - title: { - $t: "c", - type: "text" - }, - content: { - $t: "column-2: 3", - type: "text" - }, - link: [ - { - href: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cre1l", - type: "application/atom+xml", - rel: "self" - } - ], - id: { - $t: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values/cre1l" - } - } - ], - openSearch$totalResults: { - $t: "3" - }, - id: { - $t: "http://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values" - } - }, - version: "1.0", - encoding: "UTF-8" -} - -test("GDocs Backend", function() { - var dataset = new recline.Model.Dataset({ - url: 'https://spreadsheets.google.com/feeds/list/0Aon3JiuouxLUdDQwZE1JdV94cUd6NWtuZ0IyWTBjLWc/od6/public/values?alt=json', - backend: 'gdocs' - }); - - var stub = sinon.stub($, 'getJSON', function(options, cb) { - var spreadsheetUrl = 'spreadsheets.google.com/feeds/worksheets/'; - var worksheetUrl = 'spreadsheets.google.com/feeds/list/'; - - if (options.indexOf(spreadsheetUrl) !== -1) { - cb(sampleGDocsSpreadsheetMetadata) - } - else if(options.indexOf(worksheetUrl) !== -1) { - cb(sampleGDocsSpreadsheetData) - } - }); - - dataset.fetch().then(function() { - var docList = dataset.records; - deepEqual(['column-2', 'column-1'], _.pluck(dataset.fields.toJSON(), 'id')); - equal(3, docList.length); - equal("A", docList.models[0].get('column-1')); - equal('javascript-test :: Sheet1', dataset.get('title')); - }); - $.getJSON.restore(); -}); - -test("GDocs Backend.getUrl", function() { - var key = 'Abc_dajkdkjdafkj'; - var gid = 0; - var worksheet = 1; - var url = 'https://docs.google.com/spreadsheet/ccc?key=' + key + '#gid=' + gid - var out = recline.Backend.GDocs.getGDocsAPIUrls(url); - var exp1 = 'https://spreadsheets.google.com/feeds/list/' + key + '/' + worksheet + '/public/values?alt=json' - var exp2 = 'https://spreadsheets.google.com/feeds/worksheets/' + key + '/public/basic?alt=json' - equal(exp1, out.worksheet); - equal(exp2, out.spreadsheet); - - var url = 'https://docs.google.com/spreadsheet/ccc?key=' + key; - var out = recline.Backend.GDocs.getGDocsAPIUrls(url); - equal(out.worksheet, exp1); -}); - -})(this.jQuery); - diff --git a/test/index.html b/test/index.html index 1c103cac..eb0cf8d8 100644 --- a/test/index.html +++ b/test/index.html @@ -40,13 +40,11 @@ - - From c34d8c681cdc6a10d6a2cb8b6559b86f679aeaf4 Mon Sep 17 00:00:00 2001 From: David Miller Date: Thu, 1 Aug 2013 13:06:28 +0100 Subject: [PATCH 49/74] Check that all elements of a series are convertable to floats before casting individual elements. --- src/view.flot.js | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index f20a3ab3..4e111fe5 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -312,21 +312,28 @@ my.Flot = Backbone.View.extend({ _.each(this.state.attributes.series, function(field) { var points = []; var fieldLabel = self.model.fields.get(field).get('label'); - _.each(self.model.records.models, function(doc, index) { - var x = doc.getFieldValueUnrendered(xfield); - if (isDateTime) { - // cast to string as Date(1990) produces 1970 date but Date('1990') produces 1/1/1990 - var _date = moment(String(x)); - if (_date.isValid()) { - x = _date.toDate().getTime(); - } - } else if (typeof x === 'string') { - x = parseFloat(x); - if (isNaN(x)) { // assume this is a string label - x = index; - self.xvaluesAreIndex = true; - } + var raw = _.map(self.model.records.models, function(doc, index){ return doc.getFieldValueUnrendered(xfield) }); + + if (isDateTime){ + var cast = function(x){ + var _date = moment(String(x)); + if (_date.isValid()) { + x = _date.toDate().getTime(); + } + return x + } + } else if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){ + var cast = function(x){ return parseFloat(x) } + } else { + self.xvaluesAreIndex = true + } + + _.each(self.model.records.models, function(doc, index) { + if(self.xvaluesAreIndex){ + var x = index; + }else{ + var x = cast(doc.getFieldValueUnrendered(xfield)); } var yfield = self.model.fields.get(field); From 49d89cc0ea4979249967843ae9171990c2d6d809 Mon Sep 17 00:00:00 2001 From: David Miller Date: Tue, 6 Aug 2013 18:26:12 +0100 Subject: [PATCH 50/74] Only create variables if we need them. --- src/view.flot.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/view.flot.js b/src/view.flot.js index 4e111fe5..1b50f063 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -313,8 +313,6 @@ my.Flot = Backbone.View.extend({ var points = []; var fieldLabel = self.model.fields.get(field).get('label'); - var raw = _.map(self.model.records.models, function(doc, index){ return doc.getFieldValueUnrendered(xfield) }); - if (isDateTime){ var cast = function(x){ var _date = moment(String(x)); @@ -323,10 +321,17 @@ my.Flot = Backbone.View.extend({ } return x } - } else if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){ - var cast = function(x){ return parseFloat(x) } } else { - self.xvaluesAreIndex = true + var raw = _.map(self.model.records.models, + function(doc, index){ + return doc.getFieldValueUnrendered(xfield) + }); + + if (_.all(raw, function(x){ return !isNaN(parseFloat(x)) })){ + var cast = function(x){ return parseFloat(x) } + } else { + self.xvaluesAreIndex = true + } } _.each(self.model.records.models, function(doc, index) { From db9fe8d3a2cec6158cfee2a49931e9c18d670cc0 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 14 Aug 2013 16:54:53 +0100 Subject: [PATCH 51/74] [view/flot,bugfix][xs]: tiny fix to have a correct default for graph type. --- src/view.flot.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/view.flot.js b/src/view.flot.js index f20a3ab3..3ad2bdfb 100644 --- a/src/view.flot.js +++ b/src/view.flot.js @@ -15,7 +15,8 @@ this.recline.View = this.recline.View || {}; // { // group: {column name for x-axis}, // series: [{column name for series A}, {column name series B}, ... ], -// graphType: 'line', +// // options are: lines, points, lines-and-points, bars, columns +// graphType: 'lines', // graphOptions: {custom [flot options]} // } // From 3bda0d6ff8e841a75febb8bda777e9d7188a6b9d Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Wed, 14 Aug 2013 18:36:51 +0100 Subject: [PATCH 52/74] [#316,vendor][s]: introduce latest timelinejs lib (but do not switch to it). --- vendor/timeline/2.24/LICENSE | 365 + vendor/timeline/2.24/css/loading.gif | Bin 0 -> 6909 bytes vendor/timeline/2.24/css/timeline.css | 284 + vendor/timeline/2.24/css/timeline.png | Bin 0 -> 19872 bytes vendor/timeline/2.24/css/timeline@2x.png | Bin 0 -> 49364 bytes vendor/timeline/2.24/js/timeline.js | 9990 ++++++++++++++++++++++ 6 files changed, 10639 insertions(+) create mode 100644 vendor/timeline/2.24/LICENSE create mode 100644 vendor/timeline/2.24/css/loading.gif create mode 100644 vendor/timeline/2.24/css/timeline.css create mode 100644 vendor/timeline/2.24/css/timeline.png create mode 100644 vendor/timeline/2.24/css/timeline@2x.png create mode 100644 vendor/timeline/2.24/js/timeline.js diff --git a/vendor/timeline/2.24/LICENSE b/vendor/timeline/2.24/LICENSE new file mode 100644 index 00000000..d061c361 --- /dev/null +++ b/vendor/timeline/2.24/LICENSE @@ -0,0 +1,365 @@ +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + + +------------------------------------------- + +Map tiles by [Stamen Design](http://stamen.com "Stamen Design"), under +[CC BY 3.0](http://creativecommons.org/licenses/by/3.0 "CC BY 3.0"). +Data by [OpenStreetMap](http://openstreetmap.org "OpenStreetMap"), +under [CC BY SA](http://creativecommons.org/licenses/by-sa/3.0 "CC BY SA"). diff --git a/vendor/timeline/2.24/css/loading.gif b/vendor/timeline/2.24/css/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..d3eef2d69f7665878adab097a5fd3948caf02e07 GIT binary patch literal 6909 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 literal 0 HcmV?d00001 diff --git a/vendor/timeline/2.24/css/timeline.css b/vendor/timeline/2.24/css/timeline.css new file mode 100644 index 00000000..88e46824 --- /dev/null +++ b/vendor/timeline/2.24/css/timeline.css @@ -0,0 +1,284 @@ +.vco-storyjs{}.vco-storyjs div *{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} +.vco-storyjs h1,.vco-storyjs h2,.vco-storyjs h3,.vco-storyjs h4,.vco-storyjs h5,.vco-storyjs h6,.vco-storyjs p,.vco-storyjs blockquote,.vco-storyjs pre,.vco-storyjs a,.vco-storyjs abbr,.vco-storyjs acronym,.vco-storyjs address,.vco-storyjs cite,.vco-storyjs code,.vco-storyjs del,.vco-storyjs dfn,.vco-storyjs em,.vco-storyjs img,.vco-storyjs q,.vco-storyjs s,.vco-storyjs samp,.vco-storyjs small,.vco-storyjs strike,.vco-storyjs strong,.vco-storyjs sub,.vco-storyjs sup,.vco-storyjs tt,.vco-storyjs var,.vco-storyjs dd,.vco-storyjs dl,.vco-storyjs dt,.vco-storyjs li,.vco-storyjs ol,.vco-storyjs ul,.vco-storyjs fieldset,.vco-storyjs form,.vco-storyjs label,.vco-storyjs legend,.vco-storyjs button,.vco-storyjs table,.vco-storyjs caption,.vco-storyjs tbody,.vco-storyjs tfoot,.vco-storyjs thead,.vco-storyjs tr,.vco-storyjs th,.vco-storyjs td,.vco-storyjs .vco-container,.vco-storyjs .content-container,.vco-storyjs .media,.vco-storyjs .text,.vco-storyjs .vco-slider,.vco-storyjs .slider,.vco-storyjs .date,.vco-storyjs .title,.vco-storyjs .messege,.vco-storyjs .map,.vco-storyjs .credit,.vco-storyjs .caption,.vco-storyjs .vco-feedback,.vco-storyjs .vco-feature,.vco-storyjs .toolbar,.vco-storyjs .marker,.vco-storyjs .dot,.vco-storyjs .line,.vco-storyjs .flag,.vco-storyjs .time,.vco-storyjs .era,.vco-storyjs .major,.vco-storyjs .minor,.vco-storyjs .vco-navigation,.vco-storyjs .start,.vco-storyjs .active{margin:0;padding:0;border:0;font-weight:normal;font-style:normal;font-size:100%;line-height:1;font-family:inherit;width:auto;float:none;} +.vco-storyjs h1,.vco-storyjs h2,.vco-storyjs h3,.vco-storyjs h4,.vco-storyjs h5,.vco-storyjs h6{clear:none;} +.vco-storyjs table{border-collapse:collapse;border-spacing:0;} +.vco-storyjs ol,.vco-storyjs ul{list-style:none;} +.vco-storyjs q:before,.vco-storyjs q:after,.vco-storyjs blockquote:before,.vco-storyjs blockquote:after{content:"";} +.vco-storyjs a:focus{outline:thin dotted;} +.vco-storyjs a:hover,.vco-storyjs a:active{outline:0;} +.vco-storyjs article,.vco-storyjs aside,.vco-storyjs details,.vco-storyjs figcaption,.vco-storyjs figure,.vco-storyjs footer,.vco-storyjs header,.vco-storyjs hgroup,.vco-storyjs nav,.vco-storyjs section{display:block;} +.vco-storyjs audio,.vco-storyjs canvas,.vco-storyjs video{display:inline-block;*display:inline;*zoom:1;} +.vco-storyjs audio:not([controls]){display:none;} +.vco-storyjs div{max-width:none;} +.vco-storyjs sub,.vco-storyjs sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;} +.vco-storyjs sup{top:-0.5em;} +.vco-storyjs sub{bottom:-0.25em;} +.vco-storyjs img{border:0;-ms-interpolation-mode:bicubic;} +.vco-storyjs button,.vco-storyjs input,.vco-storyjs select,.vco-storyjs textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;} +.vco-storyjs button,.vco-storyjs input{line-height:normal;*overflow:visible;} +.vco-storyjs button::-moz-focus-inner,.vco-storyjs input::-moz-focus-inner{border:0;padding:0;} +.vco-storyjs button,.vco-storyjs input[type="button"],.vco-storyjs input[type="reset"],.vco-storyjs input[type="submit"]{cursor:pointer;-webkit-appearance:button;} +.vco-storyjs input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} +.vco-storyjs input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;} +.vco-storyjs textarea{overflow:auto;vertical-align:top;} +.vco-storyjs{font-family:"Georgia",Times New Roman,Times,serif;}.vco-storyjs .twitter,.vco-storyjs .vcard,.vco-storyjs .messege,.vco-storyjs .credit,.vco-storyjs .caption,.vco-storyjs .zoom-in,.vco-storyjs .zoom-out,.vco-storyjs .back-home,.vco-storyjs .time-interval div,.vco-storyjs .time-interval-major div,.vco-storyjs .nav-container{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif !important;} +.vco-storyjs h1.date,.vco-storyjs h2.date,.vco-storyjs h3.date,.vco-storyjs h4.date,.vco-storyjs h5.date,.vco-storyjs h6.date{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif !important;} +.vco-storyjs .timenav h1,.vco-storyjs .flag-content h1,.vco-storyjs .era h1,.vco-storyjs .timenav h2,.vco-storyjs .flag-content h2,.vco-storyjs .era h2,.vco-storyjs .timenav h3,.vco-storyjs .flag-content h3,.vco-storyjs .era h3,.vco-storyjs .timenav h4,.vco-storyjs .flag-content h4,.vco-storyjs .era h4,.vco-storyjs .timenav h5,.vco-storyjs .flag-content h5,.vco-storyjs .era h5,.vco-storyjs .timenav h6,.vco-storyjs .flag-content h6,.vco-storyjs .era h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif !important;} +.vco-storyjs p,.vco-storyjs blockquote,.vco-storyjs blockquote p,.vco-storyjs .twitter blockquote p{font-family:"Georgia",Times New Roman,Times,serif !important;} +.vco-storyjs .vco-feature h1,.vco-storyjs .vco-feature h2,.vco-storyjs .vco-feature h3,.vco-storyjs .vco-feature h4,.vco-storyjs .vco-feature h5,.vco-storyjs .vco-feature h6{font-family:"Georgia",Times New Roman,Times,serif;} +.timeline-tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;} +.thumbnail{background-image:url(timeline.png?v4.4);} +@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.thumbnail{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;}}.vco-storyjs{font-size:15px;font-weight:normal;line-height:20px;-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;}.vco-storyjs p{font-size:15px;font-weight:normal;line-height:20px;margin-bottom:20px;color:#666666;}.vco-storyjs p small{font-size:12px;line-height:17px;} +.vco-storyjs p:first-child{margin-top:20px;} +.vco-storyjs .vco-navigation p{color:#999999;} +.vco-storyjs .vco-feature h3,.vco-storyjs .vco-feature h4,.vco-storyjs .vco-feature h5,.vco-storyjs .vco-feature h6{margin-bottom:15px;} +.vco-storyjs .vco-feature p{color:#666666;} +.vco-storyjs .vco-feature blockquote,.vco-storyjs .vco-feature blockquote p{color:#000000;} +.vco-storyjs .date a,.vco-storyjs .title a{color:#999999;} +.vco-storyjs .hyphenate{-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto;word-wrap:break-word;} +.vco-storyjs h1,.vco-storyjs h2,.vco-storyjs h3,.vco-storyjs h4,.vco-storyjs h5,.vco-storyjs h6{font-weight:normal;color:#000000;text-transform:none;}.vco-storyjs h1 a,.vco-storyjs h2 a,.vco-storyjs h3 a,.vco-storyjs h4 a,.vco-storyjs h5 a,.vco-storyjs h6 a{color:#999999;} +.vco-storyjs h1 small,.vco-storyjs h2 small,.vco-storyjs h3 small,.vco-storyjs h4 small,.vco-storyjs h5 small,.vco-storyjs h6 small{color:#999999;} +.vco-storyjs h1.date,.vco-storyjs h2.date,.vco-storyjs h3.date,.vco-storyjs h4.date,.vco-storyjs h5.date,.vco-storyjs h6.date{font-weight:bold;} +.vco-storyjs h2.start{font-size:36px;line-height:38px;margin-bottom:15px;} +.vco-storyjs h1{margin-bottom:15px;font-size:32px;line-height:34px;}.vco-storyjs h1 small{font-size:18px;} +.vco-storyjs h2{margin-bottom:15px;font-size:28px;line-height:30px;}.vco-storyjs h2 small{font-size:14px;line-height:16px;} +.vco-storyjs h2.date{font-size:16px;line-height:18px;margin-bottom:3.75px;color:#999999;} +.vco-storyjs h3,.vco-storyjs h4,.vco-storyjs h5,.vco-storyjs h6{line-height:40px;}.vco-storyjs h3 .active,.vco-storyjs h4 .active,.vco-storyjs h5 .active,.vco-storyjs h6 .active{color:#0088cc;} +.vco-storyjs h3{font-size:28px;line-height:30px;}.vco-storyjs h3 small{font-size:14px;} +.vco-storyjs h4{font-size:20px;line-height:22px;}.vco-storyjs h4 small{font-size:12px;} +.vco-storyjs h5{font-size:16px;line-height:18px;} +.vco-storyjs h6{font-size:13px;line-height:14px;text-transform:uppercase;} +.vco-storyjs strong{font-weight:bold;font-style:inherit;} +.vco-storyjs em{font-style:italic;font-weight:inherit;} +.vco-storyjs Q{quotes:'„' '“';font-style:italic;} +.vco-storyjs blockquote,.vco-storyjs blockquote p{font-size:24px;line-height:32px;text-align:left;margin-bottom:6px;padding-top:10px;background-color:#ffffff;color:#000000;} +.vco-storyjs .credit{color:#999999;text-align:right;font-size:10px;line-height:10px;display:block;margin:0 auto;clear:both;} +.vco-storyjs .caption{text-align:left;margin-top:5px;color:#666666;font-size:11px;line-height:14px;clear:both;} +.vco-storyjs.vco-right-to-left h1,.vco-storyjs.vco-right-to-left h2,.vco-storyjs.vco-right-to-left h3,.vco-storyjs.vco-right-to-left h4,.vco-storyjs.vco-right-to-left h5,.vco-storyjs.vco-right-to-left h6,.vco-storyjs.vco-right-to-left p,.vco-storyjs.vco-right-to-left blockquote,.vco-storyjs.vco-right-to-left pre,.vco-storyjs.vco-right-to-left a,.vco-storyjs.vco-right-to-left abbr,.vco-storyjs.vco-right-to-left acronym,.vco-storyjs.vco-right-to-left address,.vco-storyjs.vco-right-to-left cite,.vco-storyjs.vco-right-to-left code,.vco-storyjs.vco-right-to-left del,.vco-storyjs.vco-right-to-left dfn,.vco-storyjs.vco-right-to-left em,.vco-storyjs.vco-right-to-left img,.vco-storyjs.vco-right-to-left q,.vco-storyjs.vco-right-to-left s,.vco-storyjs.vco-right-to-left samp,.vco-storyjs.vco-right-to-left small,.vco-storyjs.vco-right-to-left strike,.vco-storyjs.vco-right-to-left strong,.vco-storyjs.vco-right-to-left sub,.vco-storyjs.vco-right-to-left sup,.vco-storyjs.vco-right-to-left tt,.vco-storyjs.vco-right-to-left var,.vco-storyjs.vco-right-to-left dd,.vco-storyjs.vco-right-to-left dl,.vco-storyjs.vco-right-to-left dt,.vco-storyjs.vco-right-to-left li,.vco-storyjs.vco-right-to-left ol,.vco-storyjs.vco-right-to-left ul,.vco-storyjs.vco-right-to-left fieldset,.vco-storyjs.vco-right-to-left form,.vco-storyjs.vco-right-to-left label,.vco-storyjs.vco-right-to-left legend,.vco-storyjs.vco-right-to-left button,.vco-storyjs.vco-right-to-left table,.vco-storyjs.vco-right-to-left caption,.vco-storyjs.vco-right-to-left tbody,.vco-storyjs.vco-right-to-left tfoot,.vco-storyjs.vco-right-to-left thead,.vco-storyjs.vco-right-to-left tr,.vco-storyjs.vco-right-to-left th,.vco-storyjs.vco-right-to-left td{direction:rtl;} +.timeline-tooltip{position:absolute;z-index:205;display:block;visibility:visible;padding:5px;opacity:0;filter:alpha(opacity=0);font-size:15px;font-weight:bold;line-height:20px;font-size:12px;line-height:12px;} +.timeline-tooltip.in{opacity:0.8;filter:alpha(opacity=80);} +.timeline-tooltip.top{margin-top:-2px;} +.timeline-tooltip.right{margin-left:2px;} +.timeline-tooltip.bottom{margin-top:2px;} +.timeline-tooltip.left{margin-left:-2px;} +.timeline-tooltip.top .timeline-tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.timeline-tooltip.left .timeline-tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.timeline-tooltip.bottom .timeline-tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.timeline-tooltip.right .timeline-tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.timeline-tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.timeline-tooltip-arrow{position:absolute;width:0;height:0;} +@media only screen and (max-width:480px),only screen and (max-device-width:480px){.vco-slider .nav-next,.vco-slider .nav-previous{display:none;}}@media (max-width:640px){}.vco-skinny .vco-slider .slider-item .content .layout-text-media .text .container{text-align:center !important;} +.vco-skinny .vco-slider .slider-item .content .layout-text-media h2,.vco-skinny .vco-slider .slider-item .content .layout-text-media h3{display:block !important;width:100% !important;text-align:center !important;} +.vco-skinny .vco-slider .slider-item .content .content-container{display:block;}.vco-skinny .vco-slider .slider-item .content .content-container .text{width:100%;max-width:100%;min-width:120px;display:block;}.vco-skinny .vco-slider .slider-item .content .content-container .text .container{display:block;-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto;word-wrap:break-word;} +.vco-skinny .vco-slider .slider-item .content .content-container .media{width:100%;min-width:50%;float:none;}.vco-skinny .vco-slider .slider-item .content .content-container .media .media-wrapper{margin-left:0px;margin-right:0px;width:100%;display:block;} +.vco-skinny.vco-notouch .vco-slider .nav-previous,.vco-skinny.vco-notouch .vco-slider .nav-next{z-index:203;}.vco-skinny.vco-notouch .vco-slider .nav-previous .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-next .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-previous .nav-container .title,.vco-skinny.vco-notouch .vco-slider .nav-next .nav-container .title{filter:alpha(opacity=1);-khtml-opacity:0.01;-moz-opacity:0.01;opacity:0.01;} +.vco-skinny.vco-notouch .vco-slider .nav-previous .nav-container .icon,.vco-skinny.vco-notouch .vco-slider .nav-next .nav-container .icon{filter:alpha(opacity=15);-khtml-opacity:0.15;-moz-opacity:0.15;opacity:0.15;} +.vco-skinny.vco-notouch .vco-slider .nav-previous .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px 0;width:24px;height:24px;overflow:hidden;margin-left:10px;} +.vco-skinny.vco-notouch .vco-slider .nav-next .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-232px 0;width:24px;height:24px;overflow:hidden;margin-left:66px;} +.vco-skinny.vco-notouch .vco-slider .nav-previous:hover,.vco-skinny.vco-notouch .vco-slider .nav-next:hover{color:#aaaaaa !important;background-color:#333333;background-color:rgba(0, 0, 0, 0.65);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;}.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .icon,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .icon,.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .title,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .title{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;font-weight:bold;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .title,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .title{padding-bottom:5px;} +.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .date,.vco-skinny.vco-notouch .vco-slider .nav-previous:hover .nav-container .title,.vco-skinny.vco-notouch .vco-slider .nav-next:hover .nav-container .title{padding-left:5px;padding-right:5px;} +@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.vco-skinny.vco-notouch .vco-slider .nav-previous .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-208px 0;width:24px;height:24px;overflow:hidden;} .vco-skinny.vco-notouch .vco-slider .nav-next .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-232px 0;width:24px;height:24px;overflow:hidden;}}.vco-slider{width:100%;height:100%;overflow:hidden;}.vco-slider .slider-container-mask{text-align:center;width:100%;height:100%;overflow:hidden;}.vco-slider .slider-container-mask .slider-container{position:absolute;top:0px;left:-2160px;width:100%;height:100%;text-align:center;display:block;}.vco-slider .slider-container-mask .slider-container .slider-item-container{display:table-cell;vertical-align:middle;} +.vco-notouch .vco-slider .nav-previous:hover,.vco-notouch .vco-slider .nav-next:hover{color:#333333;cursor:pointer;} +.vco-notouch .vco-slider .nav-previous:hover .icon{margin-left:10px;} +.vco-notouch .vco-slider .nav-next:hover .icon{margin-left:66px;} +.vco-notouch .vco-slider .slider-item .content .content-container .media .media-container .wikipedia h4 a:hover{color:#0088cc;text-decoration:none;} +.vco-notouch .vco-slider .slider-item .content .content-container .created-at:hover{filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-notouch .vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments a:hover{text-decoration:none;}.vco-notouch .vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments a:hover h5{text-decoration:underline;} +.vco-slider img,.vco-slider embed,.vco-slider object,.vco-slider video,.vco-slider iframe{max-width:100%;} +.vco-slider .nav-previous,.vco-slider .nav-next{position:absolute;top:0px;width:100px;color:#DBDBDB;font-size:11px;}.vco-slider .nav-previous .nav-container,.vco-slider .nav-next .nav-container{height:100px;width:100px;position:absolute;} +.vco-slider .nav-previous .icon,.vco-slider .nav-next .icon{margin-top:12px;margin-bottom:15px;} +.vco-slider .nav-previous .date,.vco-slider .nav-next .date,.vco-slider .nav-previous .title,.vco-slider .nav-next .title{line-height:14px;}.vco-slider .nav-previous .date a,.vco-slider .nav-next .date a,.vco-slider .nav-previous .title a,.vco-slider .nav-next .title a{color:#999999;} +.vco-slider .nav-previous .date small,.vco-slider .nav-next .date small,.vco-slider .nav-previous .title small,.vco-slider .nav-next .title small{display:none;} +.vco-slider .nav-previous .date,.vco-slider .nav-next .date{font-size:13px;line-height:13px;font-weight:bold;text-transform:uppercase;margin-bottom:5px;} +.vco-slider .nav-previous .title,.vco-slider .nav-next .title{font-size:11px;line-height:13px;} +.vco-slider .nav-previous{float:left;text-align:left;}.vco-slider .nav-previous .icon{margin-left:15px;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-160px 0;width:24px;height:24px;overflow:hidden;} +.vco-slider .nav-previous .date,.vco-slider .nav-previous .title{text-align:left;padding-left:15px;} +.vco-slider .nav-next{float:right;text-align:right;}.vco-slider .nav-next .icon{margin-left:61px;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-184px 0;width:24px;height:24px;overflow:hidden;} +.vco-slider .nav-next .date,.vco-slider .nav-next .title{text-align:right;padding-right:15px;} +@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.vco-slider .nav-previous .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-160px 0;width:24px;height:24px;overflow:hidden;} .vco-slider .nav-next .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-184px 0;width:24px;height:24px;overflow:hidden;}}.vco-slider .slider-item{position:absolute;width:700px;height:100%;padding:0px;margin:0px;display:table;overflow-y:auto;}.vco-slider .slider-item .content{display:table-cell;vertical-align:middle;}.vco-slider .slider-item .content .pad-top .text .container{padding-top:15px;} +.vco-slider .slider-item .content .pad-right .text .container{padding-right:15px;} +.vco-slider .slider-item .content .pad-left .text .container{padding-left:30px;} +.vco-slider .slider-item .content .pad-left .media.text-media .media-wrapper .media-container{border:none;background-color:#ffffff;} +.vco-slider .slider-item .content .content-container{display:table;vertical-align:middle;}.vco-slider .slider-item .content .content-container .text{width:40%;max-width:50%;min-width:120px;display:table-cell;vertical-align:middle;}.vco-slider .slider-item .content .content-container .text .container{display:table-cell;vertical-align:middle;text-align:left;}.vco-slider .slider-item .content .content-container .text .container p{-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto;word-wrap:break-word;} +.vco-slider .slider-item .content .content-container .text .container h2.date{font-size:15px;line-height:15px;font-weight:normal;} +.vco-slider .slider-item .content .content-container .text .container .slide-tag{font-size:11px;font-weight:bold;color:#ffffff;background-color:#cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;vertical-align:baseline;white-space:nowrap;line-height:11px;padding:1px 3px 1px;margin-left:7.5px;margin-bottom:7.5px;} +.vco-slider .slider-item .content .content-container .media{width:100%;min-width:50%;float:left;}.vco-slider .slider-item .content .content-container .media .media-wrapper{display:inline-block;margin-left:auto;margin-right:auto;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container{display:inline-block;line-height:0px;padding:0px;max-height:100%;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-frame,.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-image img{border:1px solid;border-color:#cccccc #999999 #999999 #cccccc;background-color:#ffffff;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-frame iframe{background-color:#ffffff;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .soundcloud{border:0;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-image{display:inline-block;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-shadow{position:relative;z-index:1;background:#ffffff;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-shadow:before,.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-shadow:after{z-index:-1;position:absolute;content:"";bottom:15px;left:10px;width:50%;top:80%;max-width:300px;background:#999999;-webkit-box-shadow:0 15px 10px #999999;-moz-box-shadow:0 15px 10px #999999;box-shadow:0 15px 10px #999999;-webkit-transform:rotate(-2deg);-moz-transform:rotate(-2deg);-ms-transform:rotate(-2deg);-o-transform:rotate(-2deg);transform:rotate(-2deg);} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .media-shadow::after{-webkit-transform:rotate(2deg);-moz-transform:rotate(2deg);-ms-transform:rotate(2deg);-o-transform:rotate(2deg);transform:rotate(2deg);right:10px;left:auto;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .plain-text{display:table;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .plain-text .container{display:table-cell;vertical-align:middle;font-size:15px;line-height:20px;color:#666666;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .plain-text .container p{margin-bottom:20px;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .wikipedia{font-size:15px;line-height:20px;text-align:left;margin-left:auto;margin-right:auto;margin-bottom:15px;clear:both;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .wikipedia .wiki-source{margin-bottom:15px;font-size:13px;line-height:19px;font-style:italic;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .wikipedia h4{border-bottom:1px solid #cccccc;margin-bottom:5px;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .wikipedia h4 a{color:#000000;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .wikipedia p{font-size:13px;line-height:19px;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map{line-height:normal;z-index:200;text-align:left;background-color:#ffffff;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map img{max-height:none !important;max-width:none !important;border:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map .google-map{height:100%;width:100%;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map .map-attribution{position:absolute;z-index:201;bottom:0px;width:100%;overflow:hidden;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map .map-attribution .attribution-text{height:19px;overflow:hidden;-webkit-user-select:none;line-height:19px;margin-right:60px;padding-left:65px;font-family:Arial,sans-serif;font-size:10px;color:#444;white-space:nowrap;color:#ffffff;text-shadow:1px 1px 1px #333333;text-align:center;}.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .map .map-attribution .attribution-text a{color:#ffffff !important;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .credit{color:#999999;text-align:right;display:block;margin:0 auto;margin-top:6px;font-size:10px;line-height:13px;} +.vco-slider .slider-item .content .content-container .media .media-wrapper .media-container .caption{text-align:left;margin-top:10px;color:#666666;font-size:11px;line-height:14px;text-rendering:optimizeLegibility;word-wrap:break-word;} +.vco-slider .slider-item .content .content-container .media.text-media .media-wrapper .media-container{border:none;background-color:#ffffff;} +.vco-slider .slider-item .content .content-container .created-at{width:24px;height:24px;overflow:hidden;margin-left:7.5px;margin-top:2px;display:inline-block;float:right;filter:alpha(opacity=25);-khtml-opacity:0.25;-moz-opacity:0.25;opacity:0.25;} +.vco-slider .slider-item .content .content-container .storify .created-at{background-repeat:no-repeat;background-position:-328px -96px;} +.vco-slider .slider-item .content .content-container .twitter .created-at{background-repeat:no-repeat;background-position:-256px -24px;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content{font-size:13px;line-height:19px;margin-bottom:6px;padding-top:10px;background-color:#ffffff;color:#666666;}.vco-slider .slider-item .content .content-container .googleplus .googleplus-content p{font-size:13px;line-height:19px;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-title{font-size:24px;line-height:32px;margin-bottom:6px;padding-top:10px;background-color:#ffffff;color:#000000;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-annotation{font-size:15px;line-height:20px;color:#000000;border-bottom:1px solid #e3e3e3;padding-bottom:7.5px;margin-bottom:7.5px;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments{border-top:1px solid #e3e3e3;padding-top:15px;margin-top:15px;border-bottom:1px solid #e3e3e3;padding-bottom:15px;margin-bottom:15px;*zoom:1;}.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments:before,.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments:after{display:table;content:"";} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments:after{clear:both;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments h5{margin-bottom:5px;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments div{width:50%;padding-left:15px;display:inline-block;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments p{font-size:11px;line-height:14px;margin-bottom:5px;} +.vco-slider .slider-item .content .content-container .googleplus .googleplus-content .googleplus-attachments img{float:left;display:block;bottom:0;left:0;margin:auto;position:relative;right:0;top:0;width:40%;} +.vco-slider .slider-item .content .content-container .googleplus .proflinkPrefix{color:#0088cc;} +.vco-slider .slider-item .content .content-container .googleplus .created-at{background-repeat:no-repeat;background-position:-208px -72px;} +.vco-slider .slider-item .content .content-container .twitter,.vco-slider .slider-item .content .content-container .plain-text-quote,.vco-slider .slider-item .content .content-container .storify,.vco-slider .slider-item .content .content-container .googleplus{text-align:left;margin-left:auto;margin-right:auto;margin-bottom:15px;clear:both;}.vco-slider .slider-item .content .content-container .twitter blockquote,.vco-slider .slider-item .content .content-container .plain-text-quote blockquote,.vco-slider .slider-item .content .content-container .storify blockquote,.vco-slider .slider-item .content .content-container .googleplus blockquote{color:#666666;}.vco-slider .slider-item .content .content-container .twitter blockquote p,.vco-slider .slider-item .content .content-container .plain-text-quote blockquote p,.vco-slider .slider-item .content .content-container .storify blockquote p,.vco-slider .slider-item .content .content-container .googleplus blockquote p{font-size:24px;line-height:32px;margin-bottom:6px;padding-top:10px;background-color:#ffffff;color:#000000;} +.vco-slider .slider-item .content .content-container .twitter blockquote .quote-mark,.vco-slider .slider-item .content .content-container .plain-text-quote blockquote .quote-mark,.vco-slider .slider-item .content .content-container .storify blockquote .quote-mark,.vco-slider .slider-item .content .content-container .googleplus blockquote .quote-mark{color:#666666;} +.vco-slider .slider-item .content .content-container .twitter blockquote{font-size:15px;}.vco-slider .slider-item .content .content-container .twitter blockquote p{font-size:24px;} +.vco-slider .slider-item .content .content-container.layout-text-media .text-media{border-top:1px solid #e3e3e3;padding-top:15px;padding-right:0;} +.vco-slider .slider-item .content .content-container.layout-text-media.pad-left .text-media{padding-right:15px;padding-top:0;border-right:1px solid #e3e3e3;border-top:0px solid #e3e3e3;} +.vco-slider .slider-item .content .content-container.layout-text{width:100%;}.vco-slider .slider-item .content .content-container.layout-text .text{width:100%;max-width:100%;}.vco-slider .slider-item .content .content-container.layout-text .text .container{display:block;vertical-align:middle;padding:0px;width:90%;text-align:left;margin-left:auto;margin-right:auto;} +.vco-slider .slider-item .content .content-container.layout-media{width:100%;}.vco-slider .slider-item .content .content-container.layout-media .text{width:100%;height:100%;max-width:100%;display:block;text-align:center;}.vco-slider .slider-item .content .content-container.layout-media .text .container{display:block;text-align:center;width:100%;margin-left:none;margin-right:none;} +.vco-slider .slider-item .content .content-container.layout-media .media{width:100%;min-width:50%;float:none;}.vco-slider .slider-item .content .content-container.layout-media .media .media-wrapper .media-container{margin-left:auto;margin-right:auto;line-height:0px;padding:0px;} +.vco-slider .slider-item .content .content-container.layout-media .twitter,.vco-slider .slider-item .content .content-container.layout-media .wikipedia,.vco-slider .slider-item .content .content-container.layout-media .googleplus{max-width:70%;} +.storyjs-embed{background-color:#ffffff;margin-bottom:20px;border:1px solid #cccccc;padding-top:20px;padding-bottom:20px;clear:both;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;-webkit-box-shadow:1px 1px 3px rgba(0, 0, 0, 0.35);-moz-box-shadow:1px 1px 3px rgba(0, 0, 0, 0.35);box-shadow:1px 1px 3px rgba(0, 0, 0, 0.35);} +.storyjs-embed.full-embed{overflow:hidden;border:0 !important;padding:0 !important;margin:0 !important;clear:both;-webkit-border-radius:0 !important;-moz-border-radius:0 !important;border-radius:0 !important;-webkit-box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;-moz-box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;} +.storyjs-embed.sized-embed{overflow:hidden;border:1px solid #cccccc;padding-top:7px;padding-bottom:7px;margin:0 !important;clear:both;-webkit-box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;-moz-box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;box-shadow:0 0px 0px rgba(0, 0, 0, 0.25) !important;} +.vco-storyjs{width:100%;height:100%;padding:0px;margin:0px;background-color:#ffffff;position:absolute;z-index:100;clear:both;overflow:hidden;}.vco-storyjs .vmm-clear:before,.vco-storyjs .vmm-clear:after{content:"";display:table;} +.vco-storyjs .vmm-clear:after{clear:both;} +.vco-storyjs .vmm-clear{*zoom:1;} +.vco-storyjs .vco-feature{width:100%;}.vco-storyjs .vco-feature .slider,.vco-storyjs .vco-feature .vco-slider{width:100%;float:left;position:relative;z-index:10;padding-top:15px;-webkit-box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);} +.vco-storyjs .vco-feedback{position:absolute;display:table;overflow:hidden;top:0px;left:0px;z-index:205;width:100%;height:100%;} +.vco-storyjs div.vco-loading,.vco-storyjs div.vco-explainer{display:table;text-align:center;min-width:100px;margin-top:15px;height:100%;width:100%;background-color:#ffffff;}.vco-storyjs div.vco-loading .vco-loading-container,.vco-storyjs div.vco-explainer .vco-loading-container,.vco-storyjs div.vco-loading .vco-explainer-container,.vco-storyjs div.vco-explainer .vco-explainer-container{display:table-cell;vertical-align:middle;}.vco-storyjs div.vco-loading .vco-loading-container .vco-loading-icon,.vco-storyjs div.vco-explainer .vco-loading-container .vco-loading-icon,.vco-storyjs div.vco-loading .vco-explainer-container .vco-loading-icon,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-loading-icon{display:block;background-repeat:no-repeat;vertical-align:middle;margin-left:auto;margin-right:auto;text-align:center;background-image:url(loading.gif?v3.4);width:28px;height:28px;} +.vco-storyjs div.vco-loading .vco-loading-container .vco-gesture-icon,.vco-storyjs div.vco-explainer .vco-loading-container .vco-gesture-icon,.vco-storyjs div.vco-loading .vco-explainer-container .vco-gesture-icon,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-gesture-icon{display:block;vertical-align:middle;margin-left:auto;margin-right:auto;text-align:center;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-160px -160px;width:48px;height:48px;} +.vco-storyjs div.vco-loading .vco-loading-container .vco-message,.vco-storyjs div.vco-explainer .vco-loading-container .vco-message,.vco-storyjs div.vco-loading .vco-explainer-container .vco-message,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-message{display:block;} +.vco-storyjs div.vco-loading .vco-loading-container .vco-message,.vco-storyjs div.vco-explainer .vco-loading-container .vco-message,.vco-storyjs div.vco-loading .vco-explainer-container .vco-message,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-message,.vco-storyjs div.vco-loading .vco-loading-container .vco-message p,.vco-storyjs div.vco-explainer .vco-loading-container .vco-message p,.vco-storyjs div.vco-loading .vco-explainer-container .vco-message p,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-message p{text-align:center;font-size:11px;line-height:13px;text-transform:uppercase;margin-top:7.5px;margin-bottom:7.5px;} +.vco-storyjs div.vco-explainer{background-color:transparent;} +.vco-storyjs .vco-bezel{background-color:#333333;background-color:rgba(0, 0, 0, 0.8);width:80px;height:50px;padding:50px;padding-top:25px;padding:25px 20px 50px 20px;margin:auto;-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;}.vco-storyjs .vco-bezel .vco-message,.vco-storyjs .vco-bezel .vco-message p{color:#ffffff;font-weight:bold;} +.vco-storyjs .vco-container.vco-main{position:absolute;top:0px;left:0px;padding-bottom:3px;width:auto;height:auto;margin:0px;clear:both;} +.vco-storyjs img,.vco-storyjs embed,.vco-storyjs object,.vco-storyjs video,.vco-storyjs iframe{max-width:100%;} +.vco-storyjs img{max-height:100%;border:1px solid #999999;} +.vco-storyjs a{color:#0088cc;text-decoration:none;} +.vco-storyjs a:hover{color:#005580;text-decoration:underline;} +.vco-storyjs .vcard{float:right;margin-bottom:15px;}.vco-storyjs .vcard a{color:#333333;} +.vco-storyjs .vcard a:hover{text-decoration:none;}.vco-storyjs .vcard a:hover .fn{text-decoration:underline;} +.vco-storyjs .vcard .fn,.vco-storyjs .vcard .nickname{padding-left:42px;} +.vco-storyjs .vcard .fn{display:block;font-weight:bold;} +.vco-storyjs .vcard .nickname{margin-top:1px;display:block;color:#666666;} +.vco-storyjs .vcard .avatar{float:left;display:block;width:32px;height:32px;}.vco-storyjs .vcard .avatar img{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px;} +.vco-storyjs .thumbnail{width:24px;height:24px;overflow:hidden;float:left;margin:0;margin-right:1px;margin-top:6px;border:0;padding:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.vco-storyjs a.thumbnail:hover{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.vco-storyjs .thumbnail.thumb-plaintext{background-repeat:no-repeat;background-position:-280px -48px;} +.vco-storyjs .thumbnail.thumb-quote{background-repeat:no-repeat;background-position:-232px -48px;} +.vco-storyjs .thumbnail.thumb-document{background-repeat:no-repeat;background-position:-256px -48px;} +.vco-storyjs .thumbnail.thumb-photo{background-repeat:no-repeat;background-position:-280px -24px;border:0;}.vco-storyjs .thumbnail.thumb-photo img{border:0px none #cccccc !important;} +.vco-storyjs .thumbnail.thumb-twitter{background-repeat:no-repeat;background-position:-256px -24px;} +.vco-storyjs .thumbnail.thumb-vimeo{background-repeat:no-repeat;background-position:-328px -48px;} +.vco-storyjs .thumbnail.thumb-vine{background-repeat:no-repeat;background-position:-232px -72px;} +.vco-storyjs .thumbnail.thumb-youtube{background-repeat:no-repeat;background-position:-328px -72px;} +.vco-storyjs .thumbnail.thumb-video{background-repeat:no-repeat;background-position:-328px -24px;} +.vco-storyjs .thumbnail.thumb-audio{background-repeat:no-repeat;background-position:-304px -24px;} +.vco-storyjs .thumbnail.thumb-map{background-repeat:no-repeat;background-position:-208px -48px;} +.vco-storyjs .thumbnail.thumb-website{background-repeat:no-repeat;background-position:-232px -24px;} +.vco-storyjs .thumbnail.thumb-link{background-repeat:no-repeat;background-position:-184px -72px;} +.vco-storyjs .thumbnail.thumb-wikipedia{background-repeat:no-repeat;background-position:-184px -48px;} +.vco-storyjs .thumbnail.thumb-storify{background-repeat:no-repeat;background-position:-328px -96px;} +.vco-storyjs .thumbnail.thumb-googleplus{background-repeat:no-repeat;background-position:-208px -72px;} +.vco-storyjs thumbnail.thumb-instagram{background-repeat:no-repeat;background-position:-208px -96px;} +.vco-storyjs thumbnail.thumb-instagram-full{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-232px -96px;width:48px;height:24px;} +.vco-storyjs .thumb-storify-full{height:12px;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-280px -96px;width:48px;} +.vco-storyjs .thumbnail-inline{width:16px;height:14px;overflow:hidden;display:inline-block;margin-right:1px;margin-left:3px;margin-top:2px;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-storyjs .twitter .thumbnail-inline{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-160px -96px;} +.vco-storyjs .storify .thumbnail-inline{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-184px -96px;} +.vco-storyjs .googleplus .thumbnail-inline{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px -96px;} +.vco-storyjs .zFront{z-index:204;} +@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.vco-storyjs div.vco-loading .vco-loading-container .vco-loading-icon,.vco-storyjs div.vco-explainer .vco-loading-container .vco-loading-icon,.vco-storyjs div.vco-loading .vco-explainer-container .vco-loading-icon,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-loading-icon{background-image:url(loading@2x.gif?v3.4);} .vco-storyjs div.vco-loading .vco-loading-container .vco-gesture-icon,.vco-storyjs div.vco-explainer .vco-loading-container .vco-gesture-icon,.vco-storyjs div.vco-loading .vco-explainer-container .vco-gesture-icon,.vco-storyjs div.vco-explainer .vco-explainer-container .vco-gesture-icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-160px -160px;width:48px;height:48px;}}.vco-notouch .vco-navigation .vco-toolbar .zoom-in:hover,.vco-notouch .vco-navigation .vco-toolbar .zoom-out:hover,.vco-notouch .vco-navigation .vco-toolbar .back-home:hover{color:#0088cc;cursor:pointer;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-notouch .vco-navigation .timenav .content .marker.active:hover{cursor:default;}.vco-notouch .vco-navigation .timenav .content .marker.active:hover .flag .flag-content h3,.vco-notouch .vco-navigation .timenav .content .marker.active:hover .flag-small .flag-content h3{color:#0088cc;} +.vco-notouch .vco-navigation .timenav .content .marker.active:hover .flag .flag-content h4,.vco-notouch .vco-navigation .timenav .content .marker.active:hover .flag-small .flag-content h4{color:#999999;} +.vco-notouch .vco-navigation .timenav .content .marker:hover .line{z-index:24;background:#999999;} +.vco-notouch .vco-navigation .timenav .content .marker .flag:hover,.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover{cursor:pointer;}.vco-notouch .vco-navigation .timenav .content .marker .flag:hover .flag-content h3,.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover .flag-content h3{color:#333333;} +.vco-notouch .vco-navigation .timenav .content .marker .flag:hover .flag-content h4,.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover .flag-content h4{color:#aaaaaa;} +.vco-notouch .vco-navigation .timenav .content .marker .flag:hover .flag-content .thumbnail,.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover .flag-content .thumbnail{filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-notouch .vco-navigation .timenav .content .marker .flag:hover{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -53px;width:153px;height:53px;} +.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover{height:56px;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -53px;width:153px;height:53px;}.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover .flag-content{height:36px;}.vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover .flag-content h3{margin-top:5px;} +.vco-notouch .vco-navigation .timenav .content .marker .flag-small.flag-small-last:hover{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -109px;width:153px;height:26px;}.vco-notouch .vco-navigation .timenav .content .marker .flag-small.flag-small-last:hover .flag-content{height:14px;}.vco-notouch .vco-navigation .timenav .content .marker .flag-small.flag-small-last:hover .flag-content h3{margin-top:4px;} +.vco-timeline .vco-navigation{clear:both;cursor:move;width:100%;height:200px;border-top:1px solid #e3e3e3;position:relative;}.vco-timeline .vco-navigation .vco-toolbar{position:absolute;top:45px;left:0px;z-index:202;background-color:#ffffff;border:1px solid #cccccc;-webkit-box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);-moz-box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);}.vco-timeline .vco-navigation .vco-toolbar .zoom-in,.vco-timeline .vco-navigation .vco-toolbar .zoom-out,.vco-timeline .vco-navigation .vco-toolbar .back-home{font-weight:normal;font-size:10px;line-height:20px;top:0px;z-index:202;width:18px;height:18px;color:#333333;text-align:center;font-weight:bold;border:1px solid #ffffff;padding:5px;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .vco-toolbar .zoom-in .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-256px 0;width:24px;height:24px;} +.vco-timeline .vco-navigation .vco-toolbar .zoom-out .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-280px 0;width:24px;height:24px;} +.vco-timeline .vco-navigation .vco-toolbar .back-home .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-328px 0;width:24px;height:24px;} +.vco-timeline .vco-navigation .vco-toolbar.touch{-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;background-color:transparent;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.vco-timeline .vco-navigation .vco-toolbar.touch .zoom-in,.vco-timeline .vco-navigation .vco-toolbar.touch .zoom-out,.vco-timeline .vco-navigation .vco-toolbar.touch .back-home{width:40px;height:40px;padding:5px;background-color:#ffffff;border:1px solid #cccccc;-webkit-box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);-moz-box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);box-shadow:1px 1px 0px rgba(0, 0, 0, 0.2);-webkit-border-radius:10px;-moz-border-radius:10px;border-radius:10px;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-timeline .vco-navigation .vco-toolbar.touch .zoom-in .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px -160px;width:40px;height:40px;} +.vco-timeline .vco-navigation .vco-toolbar.touch .zoom-out .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-256px -160px;width:40px;height:40px;} +.vco-timeline .vco-navigation .vco-toolbar.touch .back-home .icon{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-304px -160px;width:40px;height:40px;} +.vco-timeline .vco-navigation .timenav-background{position:absolute;cursor:move;top:0px;left:0px;height:150px;width:100%;background-color:#e9e9e9;}.vco-timeline .vco-navigation .timenav-background .timenav-interval-background{position:absolute;top:151px;left:0px;background:#ffffff;width:100%;height:49px;-webkit-box-shadow:-1px -1px 7px rgba(0, 0, 0, 0.1);-moz-box-shadow:-1px -1px 7px rgba(0, 0, 0, 0.1);box-shadow:-1px -1px 7px rgba(0, 0, 0, 0.1);}.vco-timeline .vco-navigation .timenav-background .timenav-interval-background .top-highlight{position:absolute;top:-1px;left:0px;z-index:30;width:100%;height:1px;background:#ffffff;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;-webkit-box-shadow:1px 1px 5px rgba(0, 0, 0, 0.2);-moz-box-shadow:1px 1px 5px rgba(0, 0, 0, 0.2);box-shadow:1px 1px 5px rgba(0, 0, 0, 0.2);} +.vco-timeline .vco-navigation .timenav-background .timenav-line{position:absolute;top:0px;left:50%;width:3px;height:150px;background-color:#0088cc;z-index:1;-webkit-box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);box-shadow:1px 1px 7px rgba(0, 0, 0, 0.3);} +.vco-timeline .vco-navigation .timenav-background .timenav-indicator{position:absolute;top:-1px;left:50%;z-index:202;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-160px -48px;width:24px;height:24px;} +.vco-timeline .vco-navigation .timenav-background .timenav-tag div{height:50px;display:table;}.vco-timeline .vco-navigation .timenav-background .timenav-tag div h3{display:table-cell;vertical-align:middle;padding-left:65px;font-size:15px;color:#d0d0d0;font-weight:bold;text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav-background .timenav-tag-size-half{height:25px;}.vco-timeline .vco-navigation .timenav-background .timenav-tag-size-half div{height:25px;} +.vco-timeline .vco-navigation .timenav-background .timenav-tag-size-full{height:50px;}.vco-timeline .vco-navigation .timenav-background .timenav-tag-size-full div{height:50px;} +.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-2,.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-4,.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-6{background:#f1f1f1;} +.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-1,.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-3,.vco-timeline .vco-navigation .timenav-background .timenav-tag-row-5{background:#e9e9e9;} +.vco-timeline .vco-navigation .timenav{position:absolute;top:0px;left:-250px;z-index:1;}.vco-timeline .vco-navigation .timenav .content{position:relative;}.vco-timeline .vco-navigation .timenav .content .marker.start{display:none;} +.vco-timeline .vco-navigation .timenav .content .marker.active .dot{background:#0088cc;z-index:200;} +.vco-timeline .vco-navigation .timenav .content .marker.active .line{z-index:199;background:#0088cc;width:1px;}.vco-timeline .vco-navigation .timenav .content .marker.active .line .event-line{background:#0088cc;filter:alpha(opacity=75);-khtml-opacity:0.75;-moz-opacity:0.75;opacity:0.75;} +.vco-timeline .vco-navigation .timenav .content .marker.active .flag,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small{z-index:200;}.vco-timeline .vco-navigation .timenav .content .marker.active .flag .flag-content,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small .flag-content{height:36px;}.vco-timeline .vco-navigation .timenav .content .marker.active .flag .flag-content h3,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small .flag-content h3{color:#0088cc;margin-top:5px;} +.vco-timeline .vco-navigation .timenav .content .marker.active .flag .flag-content .thumbnail,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small .flag-content .thumbnail{filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.vco-timeline .vco-navigation .timenav .content .marker.active .flag.row1,.vco-timeline .vco-navigation .timenav .content .marker.active .flag.row2,.vco-timeline .vco-navigation .timenav .content .marker.active .flag.row3,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small.row1,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small.row2,.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small.row3{z-index:200;} +.vco-timeline .vco-navigation .timenav .content .marker.active .flag{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -53px;width:153px;height:53px;} +.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -109px;width:153px;height:26px;}.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small .flag-content{height:14px;}.vco-timeline .vco-navigation .timenav .content .marker.active .flag-small .flag-content h3{margin-top:4px;} +.vco-timeline .vco-navigation .timenav .content .marker{position:absolute;top:0px;left:150px;display:block;}.vco-timeline .vco-navigation .timenav .content .marker .dot{position:absolute;top:150px;left:0px;display:block;width:6px;height:6px;background:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;z-index:21;} +.vco-timeline .vco-navigation .timenav .content .marker .line{position:absolute;top:0px;left:3px;width:1px;height:150px;background-color:#cccccc;background-color:rgba(204, 204, 204, 0.5);-webkit-box-shadow:1px 0 0 rgba(255, 255, 255, 0.5);-moz-box-shadow:1px 0 0 rgba(255, 255, 255, 0.5);box-shadow:1px 0 0 rgba(255, 255, 255, 0.5);z-index:22;}.vco-timeline .vco-navigation .timenav .content .marker .line .event-line{position:absolute;z-index:22;left:0px;height:1px;width:1px;background:#0088cc;filter:alpha(opacity=15);-khtml-opacity:0.15;-moz-opacity:0.15;opacity:0.15;} +.vco-timeline .vco-navigation .timenav .content .marker .flag,.vco-timeline .vco-navigation .timenav .content .marker .flag-small{position:absolute;top:15px;left:3px;padding:0px;display:block;z-index:23;width:153px;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content{padding:0px 7px 2px 6px;overflow:hidden;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content h3,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content h3{font-weight:bold;font-size:15px;line-height:20px;font-size:11px;line-height:11px;color:#999999;margin-bottom:2px;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content h3 small,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content h3 small{display:none;} +.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content h4,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content h4{display:none;font-weight:normal;font-size:15px;line-height:20px;margin-top:5px;font-size:10px;line-height:10px;color:#aaaaaa;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content h4 small,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content h4 small{display:none;} +.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content .thumbnail,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail{margin-bottom:15px;margin-right:3px;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content .thumbnail img,.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail img{width:22px;height:22px;max-height:none;max-width:none;border:0;border:1px solid #999999;padding:0;margin:0;} +.vco-timeline .vco-navigation .timenav .content .marker .flag{height:56px;background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 0;width:153px;height:53px;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content{height:36px;}.vco-timeline .vco-navigation .timenav .content .marker .flag .flag-content h3{margin-top:5px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:0 -135px;width:153px;height:26px;}.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content{height:14px;}.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content h3{margin-top:4px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail{width:16px;height:10px;margin-right:1px;margin-top:6px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-plaintext{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-280px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-quote{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-232px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-document{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-256px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-photo{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-280px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-twitter{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-256px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-vimeo{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-328px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-vine{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-160px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-youtube{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-304px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-video{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-328px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-audio{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-304px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-map{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-website{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-232px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-link{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-232px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-wikipedia{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-184px -120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-storify{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-184px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content .thumbnail.thumb-googleplus{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px -130px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small .flag-content thumbnail.thumb-instagram{background-image:url(timeline.png?v4.4);background-repeat:no-repeat;background-position:-208px -96px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag.row1{z-index:25;top:48px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag.row2{z-index:24;top:96px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag.row3{z-index:23;top:1px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row1{z-index:28;top:24px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row2{z-index:27;top:48px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row3{z-index:26;top:72px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row4{z-index:25;top:96px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row5{z-index:24;top:120px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag-small.row6{z-index:23;top:1px;} +.vco-timeline .vco-navigation .timenav .content .marker .flag.zFront,.vco-timeline .vco-navigation .timenav .content .marker .flag-small.zFront{z-index:201;} +.vco-timeline .vco-navigation .timenav .content .era{position:absolute;top:138px;left:150px;height:12px;display:block;overflow:hidden;}.vco-timeline .vco-navigation .timenav .content .era div{height:50px;width:100%;height:100%;line-height:0px;background:#e9e9e9;background:rgba(233, 233, 233, 0.33);}.vco-timeline .vco-navigation .timenav .content .era div h3,.vco-timeline .vco-navigation .timenav .content .era div h4{position:absolute;bottom:1px;padding-left:15px;font-size:15px;font-weight:bold;color:rgba(0, 136, 204, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era1 div{background:#cc4400;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(204, 68, 0, 0.1);border-right:1px solid rgba(255, 85, 0, 0.05);}.vco-timeline .vco-navigation .timenav .content .era1 div h3,.vco-timeline .vco-navigation .timenav .content .era1 div h4{color:rgba(204, 68, 0, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era2 div{background:#cc0022;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(204, 0, 34, 0.1);border-right:1px solid rgba(255, 0, 43, 0.05);}.vco-timeline .vco-navigation .timenav .content .era2 div h3,.vco-timeline .vco-navigation .timenav .content .era2 div h4{color:rgba(204, 0, 34, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era3 div{background:#0022cc;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(0, 34, 204, 0.1);border-right:1px solid rgba(0, 43, 255, 0.05);}.vco-timeline .vco-navigation .timenav .content .era3 div h3,.vco-timeline .vco-navigation .timenav .content .era3 div h4{color:rgba(0, 34, 204, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era4 div{background:#ccaa00;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(204, 170, 0, 0.1);border-right:1px solid rgba(255, 213, 0, 0.05);}.vco-timeline .vco-navigation .timenav .content .era4 div h3,.vco-timeline .vco-navigation .timenav .content .era4 div h4{color:rgba(204, 170, 0, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era5 div{background:#00ccaa;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(0, 204, 170, 0.1);border-right:1px solid rgba(0, 255, 213, 0.05);}.vco-timeline .vco-navigation .timenav .content .era5 div h3,.vco-timeline .vco-navigation .timenav .content .era5 div h4{color:rgba(0, 204, 170, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .content .era6 div{background:#0088cc;filter:alpha(opacity=10);-khtml-opacity:0.1;-moz-opacity:0.1;opacity:0.1;border-left:1px solid rgba(0, 136, 204, 0.1);border-right:1px solid rgba(0, 170, 255, 0.05);}.vco-timeline .vco-navigation .timenav .content .era6 div h3,.vco-timeline .vco-navigation .timenav .content .era6 div h4{color:rgba(0, 136, 204, 0.35);text-shadow:0px 1px 1px #ffffff;} +.vco-timeline .vco-navigation .timenav .time{position:absolute;left:0px;top:150px;height:50px;background-color:#ffffff;line-height:0px;}.vco-timeline .vco-navigation .timenav .time .time-interval-minor{max-width:none;height:6px;white-space:nowrap;position:absolute;top:-2px;left:8px;z-index:10;}.vco-timeline .vco-navigation .timenav .time .time-interval-minor .minor{position:relative;top:2px;display:inline-block;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAMCAMAAACdvocfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRFzMzM////040VdgAAAAJ0Uk5T/wDltzBKAAAAEklEQVR42mJgYAQCBopJgAADAAbwADHy2qHzAAAAAElFTkSuQmCC);width:100px;height:6px;background-position:center top;white-space:nowrap;color:#666666;margin-top:0px;padding-top:0px;} +.vco-timeline .vco-navigation .timenav .time .time-interval{white-space:nowrap;position:absolute;top:5px;left:0px;}.vco-timeline .vco-navigation .timenav .time .time-interval div{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAMCAMAAACdvocfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRFzMzM////040VdgAAAAJ0Uk5T/wDltzBKAAAAEklEQVR42mJgYAQCBopJgAADAAbwADHy2qHzAAAAAElFTkSuQmCC);background-position:left top;background-repeat:no-repeat;padding-top:6px;position:absolute;height:3px;left:0px;display:block;font-weight:normal;font-size:10px;line-height:20px;text-transform:uppercase;text-align:left;text-indent:0px;white-space:nowrap;color:#666666;margin-left:0px;margin-right:0px;margin-top:0px;z-index:2;}.vco-timeline .vco-navigation .timenav .time .time-interval div strong{font-weight:bold;color:#000000;} +.vco-timeline .vco-navigation .timenav .time .time-interval div.era{font-weight:bold;padding-top:0px;margin-top:-3px;margin-left:2px;background-image:none;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era1{color:#cc4400;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era2{color:#cc0022;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era3{color:#0022cc;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era4{color:#ccaa00;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era5{color:#00ccaa;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval .era6{color:#0088cc;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.vco-timeline .vco-navigation .timenav .time .time-interval-major{white-space:nowrap;position:absolute;top:5px;left:0px;}.vco-timeline .vco-navigation .timenav .time .time-interval-major div{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAQAQMAAADtUYf0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyOTAzRjI3REIzNDcxMUUxQUQ3QUZCOThEODQ1NDhCNyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyOTAzRjI3RUIzNDcxMUUxQUQ3QUZCOThEODQ1NDhCNyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjI5MDNGMjdCQjM0NzExRTFBRDdBRkI5OEQ4NDU0OEI3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjI5MDNGMjdDQjM0NzExRTFBRDdBRkI5OEQ4NDU0OEI3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+DPWNfQAAAANQTFRFzMzMylJEJwAAAAtJREFUCB1jYMAPAAAgAAHDvpOtAAAAAElFTkSuQmCC);background-position:left top;background-repeat:no-repeat;padding-top:15px;position:absolute;height:15px;left:0px;display:block;font-weight:bold;font-size:12px;line-height:20px;text-transform:uppercase;text-align:left;text-indent:0px;white-space:nowrap;color:#333333;margin-left:0px;margin-right:0px;margin-top:1px;z-index:5;}.vco-timeline .vco-navigation .timenav .time .time-interval-major div strong{font-weight:bold;color:#000000;} +@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio:2){.vco-notouch .vco-navigation .vco-toolbar .zoom-in .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-256px 0;width:24px;height:24px;} .vco-notouch .vco-navigation .vco-toolbar .zoom-out .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-280px 0;width:24px;height:24px;} .vco-notouch .vco-navigation .vco-toolbar .back-home .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-328px 0;width:24px;height:24px;} .vco-notouch .vco-navigation .vco-toolbar.touch .zoom-in .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-208px -160px;width:40px;height:40px;} .vco-notouch .vco-navigation .vco-toolbar.touch .zoom-out .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-256px -160px;width:40px;height:40px;} .vco-notouch .vco-navigation .vco-toolbar.touch .back-home .icon{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-304px -160px;width:40px;height:40px;} .vco-notouch .vco-navigation .timenav .content .marker .flag:hover{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:0 -53px;width:153px;height:53px;} .vco-notouch .vco-navigation .timenav .content .marker .flag-small:hover{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:0 -53px;width:153px;height:53px;} .vco-notouch .vco-navigation .timenav .content .marker .flag-small.flag-small-last:hover{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:0 -109px;width:153px;height:26px;} .vco-notouch .vco-navigation .timenav-background .timenav-indicator{background-image:url(timeline@2x.png?v4.4);background-size:352px 260px;background-repeat:no-repeat;background-position:-160px -48px;width:24px;height:24px;}}@media screen and (-webkit-max-device-pixel-ratio:1){}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (-o-min-device-pixel-ratio:3/2),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-device-pixel-ratio:1.5){}@media screen and (max-device-width:480px) and (orientation:portrait){.storyjs-embed.full-embed{height:557px !important;width:320px !important;}.storyjs-embed.full-embed .vco-feature{height:356px !important;}}@media screen and (max-device-width:480px) and (orientation:landscape){.storyjs-embed.full-embed{height:409px !important;width:480px !important;}.storyjs-embed.full-embed .vco-feature{height:208px !important;}}@media screen and (min-device-width:481px) and (orientation:portrait){}@media screen and (min-device-width:481px) and (orientation:landscape){}@media (max-width:480px){}@media only screen and (max-width:480px){} diff --git a/vendor/timeline/2.24/css/timeline.png b/vendor/timeline/2.24/css/timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..857d0d19815d949ee5249ac90b05d5cde0551291 GIT binary patch literal 19872 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 z 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 literal 0 HcmV?d00001 diff --git a/vendor/timeline/2.24/css/timeline@2x.png b/vendor/timeline/2.24/css/timeline@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..41b4eb255a288552a13e0f23a76b28bb34333ead GIT binary patch literal 49364 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 literal 0 HcmV?d00001 diff --git a/vendor/timeline/2.24/js/timeline.js b/vendor/timeline/2.24/js/timeline.js new file mode 100644 index 00000000..d7c726bc --- /dev/null +++ b/vendor/timeline/2.24/js/timeline.js @@ -0,0 +1,9990 @@ +/*! + TimelineJS + Version 2.17 + Designed and built by Zach Wise at VéritéCo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +*/ + +/* ********************************************** + Begin VMM.StoryJS.License.js +********************************************** */ + +/*! + StoryJS + Designed and built by Zach Wise at VéritéCo + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +/* ********************************************** + Begin VMM.js +********************************************** */ + +/** + * VéritéCo JS Core + * Designed and built by Zach Wise at VéritéCo zach@verite.co + + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + +*/ + + +/* Simple JavaScript Inheritance + By John Resig http://ejohn.org/ + MIT Licensed. +================================================== */ +(function() { + var initializing = false, + fnTest = /xyz/.test(function() { + xyz; + }) ? /\b_super\b/: /.*/; + // The base Class implementation (does nothing) + this.Class = function() {}; + + // Create a new Class that inherits from this class + Class.extend = function(prop) { + var _super = this.prototype; + + // Instantiate a base class (but only create the instance, + // don't run the init constructor) + initializing = true; + var prototype = new this(); + initializing = false; + + // Copy the properties over onto the new prototype + for (var name in prop) { + // Check if we're overwriting an existing function + prototype[name] = typeof prop[name] == "function" && + typeof _super[name] == "function" && fnTest.test(prop[name]) ? + (function(name, fn) { + return function() { + var tmp = this._super; + + // Add a new ._super() method that is the same method + // but on the super-class + this._super = _super[name]; + + // The method only need to be bound temporarily, so we + // remove it when we're done executing + var ret = fn.apply(this, arguments); + this._super = tmp; + + return ret; + }; + })(name, prop[name]) : + prop[name]; + } + + // The dummy class constructor + function Class() { + // All construction is actually done in the init method + if (!initializing && this.init) + this.init.apply(this, arguments); + } + + // Populate our constructed prototype object + Class.prototype = prototype; + + // Enforce the constructor to be what we expect + Class.prototype.constructor = Class; + + // And make this class extendable + Class.extend = arguments.callee; + + return Class; + }; +})(); + +/* Access to the Global Object + access the global object without hard-coding the identifier window +================================================== */ +var global = (function () { + return this || (1,eval)('this'); +}()); + +/* VMM +================================================== */ +if (typeof VMM == 'undefined') { + + /* Main Scope Container + ================================================== */ + //var VMM = {}; + var VMM = Class.extend({}); + + /* Debug + ================================================== */ + VMM.debug = true; + + /* Master Config + ================================================== */ + + VMM.master_config = ({ + + init: function() { + return this; + }, + + sizes: { + api: { + width: 0, + height: 0 + } + }, + + vp: "Pellentesque nibh felis, eleifend id, commodo in, interdum vitae, leo", + + api_keys_master: { + flickr: "RAIvxHY4hE/Elm5cieh4X5ptMyDpj7MYIxziGxi0WGCcy1s+yr7rKQ==", + //google: "jwNGnYw4hE9lmAez4ll0QD+jo6SKBJFknkopLS4FrSAuGfIwyj57AusuR0s8dAo=", + google: "uQKadH1VMlCsp560gN2aOiMz4evWkl1s34yryl3F/9FJOsn+/948CbBUvKLN46U=", + twitter: "" + }, + + timers: { + api: 7000 + }, + + api: { + pushques: [] + + }, + + twitter: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + flickr: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + youtube: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + vimeo: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + vine: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + webthumb: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + googlemaps: { + active: false, + map_active: false, + places_active: false, + array: [], + api_loaded: false, + que: [] + }, + + googledocs: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + googleplus: { + active: false, + array: [], + api_loaded: false, + que: [] + }, + + wikipedia: { + active: false, + array: [], + api_loaded: false, + que: [], + tries: 0 + }, + + soundcloud: { + active: false, + array: [], + api_loaded: false, + que: [] + } + + }).init(); + + //VMM.createElement(tag, value, cName, attrs, styles); + VMM.createElement = function(tag, value, cName, attrs, styles) { + + var ce = ""; + + if (tag != null && tag != "") { + + // TAG + ce += "<" + tag; + if (cName != null && cName != "") { + ce += " class='" + cName + "'"; + }; + + if (attrs != null && attrs != "") { + ce += " " + attrs; + }; + + if (styles != null && styles != "") { + ce += " style='" + styles + "'"; + }; + + ce += ">"; + + if (value != null && value != "") { + ce += value; + } + + // CLOSE TAG + ce = ce + ""; + } + + return ce; + + }; + + VMM.createMediaElement = function(media, caption, credit) { + + var ce = ""; + + var _valid = false; + + ce += "
    "; + + if (media != null && media != "") { + + valid = true; + + ce += ""; + + // CREDIT + if (credit != null && credit != "") { + ce += VMM.createElement("div", credit, "credit"); + } + + // CAPTION + if (caption != null && caption != "") { + ce += VMM.createElement("div", caption, "caption"); + } + + } + + ce += "
    "; + + return ce; + + }; + + // Hide URL Bar for iOS and Android by Scott Jehl + // https://gist.github.com/1183357 + + VMM.hideUrlBar = function () { + var win = window, + doc = win.document; + + // If there's a hash, or addEventListener is undefined, stop here + if( !location.hash || !win.addEventListener ){ + + //scroll to 1 + window.scrollTo( 0, 1 ); + var scrollTop = 1, + + //reset to 0 on bodyready, if needed + bodycheck = setInterval(function(){ + if( doc.body ){ + clearInterval( bodycheck ); + scrollTop = "scrollTop" in doc.body ? doc.body.scrollTop : 1; + win.scrollTo( 0, scrollTop === 1 ? 0 : 1 ); + } + }, 15 ); + + win.addEventListener( "load", function(){ + setTimeout(function(){ + //reset to hide addr bar at onload + win.scrollTo( 0, scrollTop === 1 ? 0 : 1 ); + }, 0); + }, false ); + } + }; + + +} + +/* Trace (console.log) +================================================== */ +function trace( msg ) { + if (VMM.debug) { + if (window.console) { + console.log(msg); + } else if ( typeof( jsTrace ) != 'undefined' ) { + jsTrace.send( msg ); + } else { + //alert(msg); + } + } +} + +/* Array Remove - By John Resig (MIT Licensed) + http://ejohn.org/blog/javascript-array-remove/ +================================================== */ +Array.prototype.remove = function(from, to) { + var rest = this.slice((to || from) + 1 || this.length); + this.length = from < 0 ? this.length + from : from; + return this.push.apply(this, rest); +} + +/* Extending Date to include Week +================================================== */ +Date.prototype.getWeek = function() { + var onejan = new Date(this.getFullYear(),0,1); + return Math.ceil((((this - onejan) / 86400000) + onejan.getDay()+1)/7); +} + +/* Extending Date to include Day of Year +================================================== */ +Date.prototype.getDayOfYear = function() { + var onejan = new Date(this.getFullYear(),0,1); + return Math.ceil((this - onejan) / 86400000); +} + +/* A MORE SPECIFIC TYPEOF(); +// http://rolandog.com/archives/2007/01/18/typeof-a-more-specific-typeof/ +================================================== */ +// type.of() +var is={ + Null:function(a){return a===null;}, + Undefined:function(a){return a===undefined;}, + nt:function(a){return(a===null||a===undefined);}, + Function:function(a){return(typeof(a)==="function")?a.constructor.toString().match(/Function/)!==null:false;}, + String:function(a){return(typeof(a)==="string")?true:(typeof(a)==="object")?a.constructor.toString().match(/string/i)!==null:false;}, + Array:function(a){return(typeof(a)==="object")?a.constructor.toString().match(/array/i)!==null||a.length!==undefined:false;}, + Boolean:function(a){return(typeof(a)==="boolean")?true:(typeof(a)==="object")?a.constructor.toString().match(/boolean/i)!==null:false;}, + Date:function(a){return(typeof(a)==="date")?true:(typeof(a)==="object")?a.constructor.toString().match(/date/i)!==null:false;}, + HTML:function(a){return(typeof(a)==="object")?a.constructor.toString().match(/html/i)!==null:false;}, + Number:function(a){return(typeof(a)==="number")?true:(typeof(a)==="object")?a.constructor.toString().match(/Number/)!==null:false;}, + Object:function(a){return(typeof(a)==="object")?a.constructor.toString().match(/object/i)!==null:false;}, + RegExp:function(a){return(typeof(a)==="function")?a.constructor.toString().match(/regexp/i)!==null:false;} +}; +var type={ + of:function(a){ + for(var i in is){ + if(is[i](a)){ + return i.toLowerCase(); + } + } + } +}; + + + + + +/* ********************************************** + Begin VMM.Library.js +********************************************** */ + +/* * LIBRARY ABSTRACTION +================================================== */ +if(typeof VMM != 'undefined') { + + VMM.smoothScrollTo = function(elem, duration, ease) { + if( typeof( jQuery ) != 'undefined' ){ + var _ease = "easein", + _duration = 1000; + + if (duration != null) { + if (duration < 1) { + _duration = 1; + } else { + _duration = Math.round(duration); + } + + } + + if (ease != null && ease != "") { + _ease = ease; + } + + if (jQuery(window).scrollTop() != VMM.Lib.offset(elem).top) { + VMM.Lib.animate('html,body', _duration, _ease, {scrollTop: VMM.Lib.offset(elem).top}) + } + + } + + }; + + VMM.attachElement = function(element, content) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).html(content); + } + + }; + + VMM.appendElement = function(element, content) { + + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).append(content); + } + + }; + + VMM.getHTML = function(element) { + var e; + if( typeof( jQuery ) != 'undefined' ){ + e = jQuery(element).html(); + return e; + } + + }; + + VMM.getElement = function(element, p) { + var e; + if( typeof( jQuery ) != 'undefined' ){ + if (p) { + e = jQuery(element).parent().get(0); + + } else { + e = jQuery(element).get(0); + } + return e; + } + + }; + + VMM.bindEvent = function(element, the_handler, the_event_type, event_data) { + var e; + var _event_type = "click"; + var _event_data = {}; + + if (the_event_type != null && the_event_type != "") { + _event_type = the_event_type; + } + + if (_event_data != null && _event_data != "") { + _event_data = event_data; + } + + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).bind(_event_type, _event_data, the_handler); + + //return e; + } + + }; + + VMM.unbindEvent = function(element, the_handler, the_event_type) { + var e; + var _event_type = "click"; + var _event_data = {}; + + if (the_event_type != null && the_event_type != "") { + _event_type = the_event_type; + } + + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).unbind(_event_type, the_handler); + + //return e; + } + + }; + + VMM.fireEvent = function(element, the_event_type, the_data) { + var e; + var _event_type = "click"; + var _data = []; + + if (the_event_type != null && the_event_type != "") { + _event_type = the_event_type; + } + if (the_data != null && the_data != "") { + _data = the_data; + } + + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).trigger(_event_type, _data); + + //return e; + } + + }; + + VMM.getJSON = function(url, data, callback) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery.ajaxSetup({ + timeout: 3000 + }); + /* CHECK FOR IE + ================================================== */ + if ( VMM.Browser.browser == "Explorer" && parseInt(VMM.Browser.version, 10) >= 7 && window.XDomainRequest) { + trace("IE JSON"); + var ie_url = url; + if (ie_url.match('^http://')){ + return jQuery.getJSON(ie_url, data, callback); + } else if (ie_url.match('^https://')) { + ie_url = ie_url.replace("https://","http://"); + return jQuery.getJSON(ie_url, data, callback); + } else { + return jQuery.getJSON(url, data, callback); + } + + } else { + return jQuery.getJSON(url, data, callback); + + } + } + } + + VMM.parseJSON = function(the_json) { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery.parseJSON(the_json); + } + } + + // ADD ELEMENT AND RETURN IT + VMM.appendAndGetElement = function(append_to_element, tag, cName, content) { + var e, + _tag = "
    ", + _class = "", + _content = "", + _id = ""; + + if (tag != null && tag != "") { + _tag = tag; + } + + if (cName != null && cName != "") { + _class = cName; + } + + if (content != null && content != "") { + _content = content; + } + + if( typeof( jQuery ) != 'undefined' ){ + + e = jQuery(tag); + + e.addClass(_class); + e.html(_content); + + jQuery(append_to_element).append(e); + + } + + return e; + + }; + + VMM.Lib = { + + init: function() { + return this; + }, + + hide: function(element, duration) { + if (duration != null && duration != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).hide(duration); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).hide(); + } + } + + }, + + remove: function(element) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).remove(); + } + }, + + detach: function(element) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).detach(); + } + }, + + append: function(element, value) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).append(value); + } + }, + + prepend: function(element, value) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).prepend(value); + } + }, + + show: function(element, duration) { + if (duration != null && duration != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).show(duration); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).show(); + } + } + + }, + + load: function(element, callback_function, event_data) { + var _event_data = {elem:element}; // return element by default + if (_event_data != null && _event_data != "") { + _event_data = event_data; + } + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).load(_event_data, callback_function); + } + }, + + addClass: function(element, cName) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).addClass(cName); + } + }, + + removeClass: function(element, cName) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).removeClass(cName); + } + }, + + attr: function(element, aName, value) { + if (value != null && value != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).attr(aName, value); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).attr(aName); + } + } + }, + + prop: function(element, aName, value) { + if (typeof jQuery == 'undefined' || !/[1-9]\.[3-9].[1-9]/.test(jQuery.fn.jquery)) { + VMM.Lib.attribute(element, aName, value); + } else { + jQuery(element).prop(aName, value); + } + }, + + attribute: function(element, aName, value) { + + if (value != null && value != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).attr(aName, value); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).attr(aName); + } + } + }, + + visible: function(element, show) { + if (show != null) { + if( typeof( jQuery ) != 'undefined' ){ + if (show) { + jQuery(element).show(0); + } else { + jQuery(element).hide(0); + } + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + if ( jQuery(element).is(':visible')){ + return true; + } else { + return false; + } + } + } + }, + + css: function(element, prop, value) { + + if (value != null && value != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).css(prop, value); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).css(prop); + } + } + }, + + cssmultiple: function(element, propval) { + + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).css(propval); + } + }, + + offset: function(element) { + var p; + if( typeof( jQuery ) != 'undefined' ){ + p = jQuery(element).offset(); + } + return p; + }, + + position: function(element) { + var p; + if( typeof( jQuery ) != 'undefined' ){ + p = jQuery(element).position(); + } + return p; + }, + + width: function(element, s) { + if (s != null && s != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).width(s); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).width(); + } + } + }, + + height: function(element, s) { + if (s != null && s != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).height(s); + } + } else { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).height(); + } + } + }, + + toggleClass: function(element, cName) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).toggleClass(cName); + } + }, + + each:function(element, return_function) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).each(return_function); + } + + }, + + html: function(element, str) { + var e; + if( typeof( jQuery ) != 'undefined' ){ + e = jQuery(element).html(); + return e; + } + + if (str != null && str != "") { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).html(str); + } + } else { + var e; + if( typeof( jQuery ) != 'undefined' ){ + e = jQuery(element).html(); + return e; + } + } + + }, + + find: function(element, selec) { + if( typeof( jQuery ) != 'undefined' ){ + return jQuery(element).find(selec); + } + }, + + stop: function(element) { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).stop(); + } + }, + + delay_animate: function(delay, element, duration, ease, att, callback_function) { + if (VMM.Browser.device == "mobile" || VMM.Browser.device == "tablet") { + var _tdd = Math.round((duration/1500)*10)/10, + __duration = _tdd + 's'; + + VMM.Lib.css(element, '-webkit-transition', 'all '+ __duration + ' ease'); + VMM.Lib.css(element, '-moz-transition', 'all '+ __duration + ' ease'); + VMM.Lib.css(element, '-o-transition', 'all '+ __duration + ' ease'); + VMM.Lib.css(element, '-ms-transition', 'all '+ __duration + ' ease'); + VMM.Lib.css(element, 'transition', 'all '+ __duration + ' ease'); + VMM.Lib.cssmultiple(element, _att); + } else { + if( typeof( jQuery ) != 'undefined' ){ + jQuery(element).delay(delay).animate(att, {duration:duration, easing:ease} ); + } + } + + }, + + animate: function(element, duration, ease, att, que, callback_function) { + + var _ease = "easein", + _que = false, + _duration = 1000, + _att = {}; + + if (duration != null) { + if (duration < 1) { + _duration = 1; + } else { + _duration = Math.round(duration); + } + + } + + if (ease != null && ease != "") { + _ease = ease; + } + + if (que != null && que != "") { + _que = que; + } + + + if (att != null) { + _att = att + } else { + _att = {opacity: 0} + } + + + if (VMM.Browser.device == "mobile" || VMM.Browser.device == "tablet") { + + var _tdd = Math.round((_duration/1500)*10)/10, + __duration = _tdd + 's'; + + _ease = " cubic-bezier(0.33, 0.66, 0.66, 1)"; + //_ease = " ease-in-out"; + for (x in _att) { + if (Object.prototype.hasOwnProperty.call(_att, x)) { + trace(x + " to " + _att[x]); + VMM.Lib.css(element, '-webkit-transition', x + ' ' + __duration + _ease); + VMM.Lib.css(element, '-moz-transition', x + ' ' + __duration + _ease); + VMM.Lib.css(element, '-o-transition', x + ' ' + __duration + _ease); + VMM.Lib.css(element, '-ms-transition', x + ' ' + __duration + _ease); + VMM.Lib.css(element, 'transition', x + ' ' + __duration + _ease); + } + } + + VMM.Lib.cssmultiple(element, _att); + + } else { + if( typeof( jQuery ) != 'undefined' ){ + if (callback_function != null && callback_function != "") { + jQuery(element).animate(_att, {queue:_que, duration:_duration, easing:_ease, complete:callback_function} ); + } else { + jQuery(element).animate(_att, {queue:_que, duration:_duration, easing:_ease} ); + } + } + } + + } + + } +} + +if( typeof( jQuery ) != 'undefined' ){ + + /* XDR AJAX EXTENTION FOR jQuery + https://github.com/jaubourg/ajaxHooks/blob/master/src/ajax/xdr.js + ================================================== */ + (function( jQuery ) { + if ( window.XDomainRequest ) { + jQuery.ajaxTransport(function( s ) { + if ( s.crossDomain && s.async ) { + if ( s.timeout ) { + s.xdrTimeout = s.timeout; + delete s.timeout; + } + var xdr; + return { + send: function( _, complete ) { + function callback( status, statusText, responses, responseHeaders ) { + xdr.onload = xdr.onerror = xdr.ontimeout = jQuery.noop; + xdr = undefined; + complete( status, statusText, responses, responseHeaders ); + } + xdr = new XDomainRequest(); + xdr.open( s.type, s.url ); + xdr.onload = function() { + callback( 200, "OK", { text: xdr.responseText }, "Content-Type: " + xdr.contentType ); + }; + xdr.onerror = function() { + callback( 404, "Not Found" ); + }; + if ( s.xdrTimeout ) { + xdr.ontimeout = function() { + callback( 0, "timeout" ); + }; + xdr.timeout = s.xdrTimeout; + } + xdr.send( ( s.hasContent && s.data ) || null ); + }, + abort: function() { + if ( xdr ) { + xdr.onerror = jQuery.noop(); + xdr.abort(); + } + } + }; + } + }); + } + })( jQuery ); + + /* jQuery Easing v1.3 + http://gsgd.co.uk/sandbox/jquery/easing/ + ================================================== */ + jQuery.easing['jswing'] = jQuery.easing['swing']; + + jQuery.extend( jQuery.easing, { + def: 'easeOutQuad', + swing: function (x, t, b, c, d) { + //alert(jQuery.easing.default); + return jQuery.easing[jQuery.easing.def](x, t, b, c, d); + }, + easeInExpo: function (x, t, b, c, d) { + return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b; + }, + easeOutExpo: function (x, t, b, c, d) { + return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; + }, + easeInOutExpo: function (x, t, b, c, d) { + if (t==0) return b; + if (t==d) return b+c; + if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b; + return c/2 * (-Math.pow(2, -10 * --t) + 2) + b; + }, + easeInQuad: function (x, t, b, c, d) { + return c*(t/=d)*t + b; + }, + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + }, + easeInOutQuad: function (x, t, b, c, d) { + if ((t/=d/2) < 1) return c/2*t*t + b; + return -c/2 * ((--t)*(t-2) - 1) + b; + } + }); +} + + +/* ********************************************** + Begin VMM.Browser.js +********************************************** */ + +/* * DEVICE AND BROWSER DETECTION +================================================== */ +if(typeof VMM != 'undefined' && typeof VMM.Browser == 'undefined') { + + VMM.Browser = { + init: function () { + this.browser = this.searchString(this.dataBrowser) || "An unknown browser"; + this.version = this.searchVersion(navigator.userAgent) + || this.searchVersion(navigator.appVersion) + || "an unknown version"; + this.OS = this.searchString(this.dataOS) || "an unknown OS"; + this.device = this.searchDevice(navigator.userAgent); + this.orientation = this.searchOrientation(window.orientation); + }, + searchOrientation: function(orientation) { + var orient = ""; + if ( orientation == 0 || orientation == 180) { + orient = "portrait"; + } else if ( orientation == 90 || orientation == -90) { + orient = "landscape"; + } else { + orient = "normal"; + } + return orient; + }, + searchDevice: function(d) { + var device = ""; + if (d.match(/Android/i) || d.match(/iPhone|iPod/i)) { + device = "mobile"; + } else if (d.match(/iPad/i)) { + device = "tablet"; + } else if (d.match(/BlackBerry/i) || d.match(/IEMobile/i)) { + device = "other mobile"; + } else { + device = "desktop"; + } + return device; + }, + searchString: function (data) { + for (var i=0;i'mmmm d',' yyyy''", + full_long: "mmm d',' yyyy 'at' hh:MM TT", + full_long_small_date: "hh:MM TT'
    mmm d',' yyyy''" + }, + + month: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], + month_abbr: ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."], + day: ["Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], + day_abbr: ["Sun.", "Mon.", "Tues.", "Wed.", "Thurs.", "Fri.", "Sat."], + hour: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + hour_suffix: ["am"], + + //B.C. + bc_format: { + year: "yyyy", + month_short: "mmm", + month: "mmmm yyyy", + full_short: "mmm d", + full: "mmmm d',' yyyy", + time_no_seconds_short: "h:MM TT", + time_no_seconds_small_date: "dddd', 'h:MM TT'
    'mmmm d',' yyyy''", + full_long: "dddd',' mmm d',' yyyy 'at' hh:MM TT", + full_long_small_date: "hh:MM TT'
    'dddd',' mmm d',' yyyy''" + }, + + setLanguage: function(lang) { + trace("SET DATE LANGUAGE"); + VMM.Date.dateformats = lang.dateformats; + VMM.Date.month = lang.date.month; + VMM.Date.month_abbr = lang.date.month_abbr; + VMM.Date.day = lang.date.day; + VMM.Date.day_abbr = lang.date.day_abbr; + dateFormat.i18n.dayNames = lang.date.day_abbr.concat(lang.date.day); + dateFormat.i18n.monthNames = lang.date.month_abbr.concat(lang.date.month); + }, + + parse: function(d, precision) { + "use strict"; + var date, + date_array, + time_array, + time_parse, + p = { + year: false, + month: false, + day: false, + hour: false, + minute: false, + second: false, + millisecond: false + }; + + if (type.of(d) == "date") { + date = d; + } else { + date = new Date(0, 0, 1, 0, 0, 0, 0); + + if ( d.match(/,/gi) ) { + date_array = d.split(","); + for(var i = 0; i < date_array.length; i++) { + date_array[i] = parseInt(date_array[i], 10); + } + if (date_array[0]) { + date.setFullYear(date_array[0]); + p.year = true; + } + if (date_array[1]) { + date.setMonth(date_array[1] - 1); + p.month = true; + } + if (date_array[2]) { + date.setDate(date_array[2]); + p.day = true; + } + if (date_array[3]) { + date.setHours(date_array[3]); + p.hour = true; + } + if (date_array[4]) { + date.setMinutes(date_array[4]); + p.minute = true; + } + if (date_array[5]) { + date.setSeconds(date_array[5]); + p.second = true; + } + if (date_array[6]) { + date.setMilliseconds(date_array[6]); + p.millisecond = true; + } + } else if (d.match("/")) { + if (d.match(" ")) { + + time_parse = d.split(" "); + if (d.match(":")) { + time_array = time_parse[1].split(":"); + if (time_array[0] >= 0 ) { + date.setHours(time_array[0]); + p.hour = true; + } + if (time_array[1] >= 0) { + date.setMinutes(time_array[1]); + p.minute = true; + } + if (time_array[2] >= 0) { + date.setSeconds(time_array[2]); + p.second = true; + } + if (time_array[3] >= 0) { + date.setMilliseconds(time_array[3]); + p.millisecond = true; + } + } + date_array = time_parse[0].split("/"); + } else { + date_array = d.split("/"); + } + if (date_array[2]) { + date.setFullYear(date_array[2]); + p.year = true; + } + if (date_array[0] >= 0) { + date.setMonth(date_array[0] - 1); + p.month = true; + } + if (date_array[1] >= 0) { + if (date_array[1].length > 2) { + date.setFullYear(date_array[1]); + p.year = true; + } else { + date.setDate(date_array[1]); + p.day = true; + } + } + } else if (d.match("now")) { + var now = new Date(); + + date.setFullYear(now.getFullYear()); + p.year = true; + + date.setMonth(now.getMonth()); + p.month = true; + + date.setDate(now.getDate()); + p.day = true; + + if (d.match("hours")) { + date.setHours(now.getHours()); + p.hour = true; + } + if (d.match("minutes")) { + date.setHours(now.getHours()); + date.setMinutes(now.getMinutes()); + p.hour = true; + p.minute = true; + } + if (d.match("seconds")) { + date.setHours(now.getHours()); + date.setMinutes(now.getMinutes()); + date.setSeconds(now.getSeconds()); + p.hour = true; + p.minute = true; + p.second = true; + } + if (d.match("milliseconds")) { + date.setHours(now.getHours()); + date.setMinutes(now.getMinutes()); + date.setSeconds(now.getSeconds()); + date.setMilliseconds(now.getMilliseconds()); + p.hour = true; + p.minute = true; + p.second = true; + p.millisecond = true; + } + } else if (d.length <= 5) { + p.year = true; + date.setFullYear(parseInt(d, 10)); + date.setMonth(0); + date.setDate(1); + date.setHours(0); + date.setMinutes(0); + date.setSeconds(0); + date.setMilliseconds(0); + } else if (d.match("T")) { + if (navigator.userAgent.match(/MSIE\s(?!9.0)/)) { + // IE 8 < Won't accept dates with a "-" in them. + time_parse = d.split("T"); + if (d.match(":")) { + time_array = time_parse[1].split(":"); + if (time_array[0] >= 1) { + date.setHours(time_array[0]); + p.hour = true; + } + if (time_array[1] >= 1) { + date.setMinutes(time_array[1]); + p.minute = true; + } + if (time_array[2] >= 1) { + date.setSeconds(time_array[2]); + p.second = true; + } + if (time_array[3] >= 1) { + date.setMilliseconds(time_array[3]); + p.millisecond = true; + } + } + date_array = time_parse[0].split("-"); + if (date_array[0]) { + date.setFullYear(date_array[0]); + p.year = true; + } + if (date_array[1] >= 0) { + date.setMonth(date_array[1] - 1); + p.month = true; + } + if (date_array[2] >= 0) { + date.setDate(date_array[2]); + p.day = true; + } + + } else { + date = new Date(Date.parse(d)); + } + } else { + p.year = true; + p.month = true; + p.day = true; + p.hour = true; + p.minute = true; + p.second = true; + p.millisecond = true; + date = new Date( + parseInt(d.slice(0,4), 10), + parseInt(d.slice(4,6), 10) - 1, + parseInt(d.slice(6,8), 10), + parseInt(d.slice(8,10), 10), + parseInt(d.slice(10,12), 10) + ); + } + + } + + if (precision != null && precision != "") { + return { + date: date, + precision: p + }; + } else { + return date; + } + }, + + + + prettyDate: function(d, is_abbr, p, d2) { + var _date, + _date2, + format, + bc_check, + is_pair = false, + bc_original, + bc_number, + bc_string; + + if (d2 != null && d2 != "" && typeof d2 != 'undefined') { + is_pair = true; + trace("D2 " + d2); + } + + + if (type.of(d) == "date") { + + if (type.of(p) == "object") { + if (p.millisecond || p.second || p.minute) { + // YEAR MONTH DAY HOUR MINUTE + if (is_abbr){ + format = VMM.Date.dateformats.time_no_seconds_short; + } else { + format = VMM.Date.dateformats.time_no_seconds_small_date; + } + } else if (p.hour) { + // YEAR MONTH DAY HOUR + if (is_abbr) { + format = VMM.Date.dateformats.time_no_seconds_short; + } else { + format = VMM.Date.dateformats.time_no_seconds_small_date; + } + } else if (p.day) { + // YEAR MONTH DAY + if (is_abbr) { + format = VMM.Date.dateformats.full_short; + } else { + format = VMM.Date.dateformats.full; + } + } else if (p.month) { + // YEAR MONTH + if (is_abbr) { + format = VMM.Date.dateformats.month_short; + } else { + format = VMM.Date.dateformats.month; + } + } else if (p.year) { + format = VMM.Date.dateformats.year; + } else { + format = VMM.Date.dateformats.year; + } + + } else { + + if (d.getMonth() === 0 && d.getDate() == 1 && d.getHours() === 0 && d.getMinutes() === 0 ) { + // YEAR ONLY + format = VMM.Date.dateformats.year; + } else if (d.getDate() <= 1 && d.getHours() === 0 && d.getMinutes() === 0) { + // YEAR MONTH + if (is_abbr) { + format = VMM.Date.dateformats.month_short; + } else { + format = VMM.Date.dateformats.month; + } + } else if (d.getHours() === 0 && d.getMinutes() === 0) { + // YEAR MONTH DAY + if (is_abbr) { + format = VMM.Date.dateformats.full_short; + } else { + format = VMM.Date.dateformats.full; + } + } else if (d.getMinutes() === 0) { + // YEAR MONTH DAY HOUR + if (is_abbr) { + format = VMM.Date.dateformats.time_no_seconds_short; + } else { + format = VMM.Date.dateformats.time_no_seconds_small_date; + } + } else { + // YEAR MONTH DAY HOUR MINUTE + if (is_abbr){ + format = VMM.Date.dateformats.time_no_seconds_short; + } else { + format = VMM.Date.dateformats.full_long; + } + } + } + + _date = dateFormat(d, format, false); + //_date = "Jan" + bc_check = _date.split(" "); + + // BC TIME SUPPORT + for(var i = 0; i < bc_check.length; i++) { + if ( parseInt(bc_check[i], 10) < 0 ) { + trace("YEAR IS BC"); + bc_original = bc_check[i]; + bc_number = Math.abs( parseInt(bc_check[i], 10) ); + bc_string = bc_number.toString() + " B.C."; + _date = _date.replace(bc_original, bc_string); + } + } + + + if (is_pair) { + _date2 = dateFormat(d2, format, false); + bc_check = _date2.split(" "); + // BC TIME SUPPORT + for(var j = 0; j < bc_check.length; j++) { + if ( parseInt(bc_check[j], 10) < 0 ) { + trace("YEAR IS BC"); + bc_original = bc_check[j]; + bc_number = Math.abs( parseInt(bc_check[j], 10) ); + bc_string = bc_number.toString() + " B.C."; + _date2 = _date2.replace(bc_original, bc_string); + } + } + + } + } else { + trace("NOT A VALID DATE?"); + trace(d); + } + + if (is_pair) { + return _date + " — " + _date2; + } else { + return _date; + } + } + + }).init(); + + /* + * Date Format 1.2.3 + * (c) 2007-2009 Steven Levithan + * MIT license + * + * Includes enhancements by Scott Trenda + * and Kris Kowal + * + * Accepts a date, a mask, or a date and a mask. + * Returns a formatted version of the given date. + * The date defaults to the current date/time. + * The mask defaults to dateFormat.masks.default. + */ + + var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; + + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; + + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } + + // Passing date through Date applies Date.parse, if necessary + // Caused problems in IE + // date = date ? new Date(date) : new Date; + if (isNaN(date)) { + trace("invalid date " + date); + //return ""; + } + + mask = String(dF.masks[mask] || mask || dF.masks["default"]); + + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } + + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; + + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; + }(); + + // Some common format strings + dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" + }; + + // Internationalization strings + dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] + }; + + // For convenience... + Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); + }; + +} + +/* ********************************************** + Begin VMM.Util.js +********************************************** */ + +/* * Utilities and Useful Functions +================================================== */ +if(typeof VMM != 'undefined' && typeof VMM.Util == 'undefined') { + + VMM.Util = ({ + + init: function() { + return this; + }, + + /* * CORRECT PROTOCOL (DOES NOT WORK) + ================================================== */ + correctProtocol: function(url) { + var loc = (window.parent.location.protocol).toString(), + prefix = "", + the_url = url.split("://", 2); + + if (loc.match("http")) { + prefix = loc; + } else { + prefix = "https"; + } + + return prefix + "://" + the_url[1]; + + }, + + /* * MERGE CONFIG + ================================================== */ + mergeConfig: function(config_main, config_to_merge) { + var x; + for (x in config_to_merge) { + if (Object.prototype.hasOwnProperty.call(config_to_merge, x)) { + config_main[x] = config_to_merge[x]; + } + } + return config_main; + }, + + /* * GET OBJECT ATTRIBUTE BY INDEX + ================================================== */ + getObjectAttributeByIndex: function(obj, index) { + if(typeof obj != 'undefined') { + var i = 0; + for (var attr in obj){ + if (index === i){ + return obj[attr]; + } + i++; + } + return ""; + } else { + return ""; + } + + }, + + /* * ORDINAL + ================================================== */ + ordinal: function(n) { + return ["th","st","nd","rd"][(!( ((n%10) >3) || (Math.floor(n%100/10)==1)) ) * (n%10)]; + }, + + /* * RANDOM BETWEEN + ================================================== */ + //VMM.Util.randomBetween(1, 3) + randomBetween: function(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); + }, + + /* * AVERAGE + * http://jsfromhell.com/array/average + * var x = VMM.Util.average([2, 3, 4]); + * VMM.Util.average([2, 3, 4]).mean + ================================================== */ + average: function(a) { + var r = {mean: 0, variance: 0, deviation: 0}, t = a.length; + for(var m, s = 0, l = t; l--; s += a[l]); + for(m = r.mean = s / t, l = t, s = 0; l--; s += Math.pow(a[l] - m, 2)); + return r.deviation = Math.sqrt(r.variance = s / t), r; + }, + + /* * CUSTOM SORT + ================================================== */ + customSort: function(a, b) { + var a1= a, b1= b; + if(a1== b1) return 0; + return a1> b1? 1: -1; + }, + + /* * Remove Duplicates from Array + ================================================== */ + deDupeArray: function(arr) { + var i, + len=arr.length, + out=[], + obj={}; + + for (i=0;i h) { + _fit.height = h; + //_fit.width = Math.round((w / ratio_w) * ratio_h); + _fit.width = Math.round((h / ratio_h) * ratio_w); + + if (_fit.width > w) { + trace("FIT: DIDN'T FIT!!! ") + } + } + + return _fit; + + }, + r16_9: function(w,h) { + //VMM.Util.ratio.r16_9(w, h) // Returns corresponding number + if (w !== null && w !== "") { + return Math.round((h / 16) * 9); + } else if (h !== null && h !== "") { + return Math.round((w / 9) * 16); + } + }, + r4_3: function(w,h) { + if (w !== null && w !== "") { + return Math.round((h / 4) * 3); + } else if (h !== null && h !== "") { + return Math.round((w / 3) * 4); + } + } + }, + + doubledigit: function(n) { + return (n < 10 ? '0' : '') + n; + }, + + /* * Returns a truncated segement of a long string of between min and max words. If possible, ends on a period (otherwise goes to max). + ================================================== */ + truncateWords: function(s, min, max) { + + if (!min) min = 30; + if (!max) max = min; + + var initial_whitespace_rExp = /^[^A-Za-z0-9\'\-]+/gi; + var left_trimmedStr = s.replace(initial_whitespace_rExp, ""); + var words = left_trimmedStr.split(" "); + + var result = []; + + min = Math.min(words.length, min); + max = Math.min(words.length, max); + + for (var i = 0; i$&") + .replace(pseudoUrlPattern, "$1$2") + .replace(emailAddressPattern, "$1"); + }, + + linkify_with_twitter: function(text,targets,is_touch) { + + // http://, https://, ftp:// + var urlPattern = /\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim; + var url_pattern = /(\()((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\))|(\[)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\])|(\{)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(\})|(<|&(?:lt|#60|#x3c);)((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]+)(>|&(?:gt|#62|#x3e);)|((?:^|[^=\s'"\]])\s*['"]?|[^=\s]\s+)(\b(?:ht|f)tps?:\/\/[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]+(?:(?!&(?:gt|#0*62|#x0*3e);|&(?:amp|apos|quot|#0*3[49]|#x0*2[27]);[.!&',:?;]?(?:[^a-z0-9\-._~!$&'()*+,;=:\/?#[\]@%]|$))&[a-z0-9\-._~!$'()*+,;=:\/?#[\]@%]*)*[a-z0-9\-_~$()*+=\/#[\]@%])/img; + var url_replace = '$1$4$7$10$13$2$5$8$11$14$3$6$9$12'; + + // www. sans http:// or https:// + var pseudoUrlPattern = /(^|[^\/])(www\.[\S]+(\b|$))/gim; + function replaceURLWithHTMLLinks(text) { + var exp = /(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?\/=~_|!:,.;]*)[-A-Z0-9+&@#\/%=~_|])/ig; + return text.replace(exp, "$3"); + } + // Email addresses + var emailAddressPattern = /(([a-zA-Z0-9_\-\.]+)@[a-zA-Z_]+?(?:\.[a-zA-Z]{2,6}))+/gim; + + //var twitterHandlePattern = /(@([\w]+))/g; + var twitterHandlePattern = /\B@([\w-]+)/gm; + var twitterSearchPattern = /(#([\w]+))/g; + + return text + //.replace(urlPattern, "$&") + .replace(url_pattern, url_replace) + .replace(pseudoUrlPattern, "$1$2") + .replace(emailAddressPattern, "$1") + .replace(twitterHandlePattern, "@$1"); + + // TURN THIS BACK ON TO AUTOMAGICALLY LINK HASHTAGS TO TWITTER SEARCH + //.replace(twitterSearchPattern, "$1"); + }, + + linkify_wikipedia: function(text) { + + var urlPattern = /]*>(.*?)<\/i>/gim; + return text + .replace(urlPattern, "$&") + .replace(/]*>/gim, "") + .replace(/<\/i>/gim, "") + .replace(/]*>/gim, "") + .replace(/<\/b>/gim, ""); + }, + + /* * Turns plain text links into real links + ================================================== */ + // VMM.Util.unlinkify(); + unlinkify: function(text) { + if(!text) return text; + text = text.replace(/]*>/i,""); + text = text.replace(/<\/a>/i, ""); + return text; + }, + + untagify: function(text) { + if (!text) { + return text; + } + text = text.replace(/<\s*\w.*?>/g,""); + return text; + }, + + /* * TK + ================================================== */ + nl2br: function(text) { + return text.replace(/(\r\n|[\r\n]|\\n|\\r)/g,"
    "); + }, + + /* * Generate a Unique ID + ================================================== */ + // VMM.Util.unique_ID(size); + unique_ID: function(size) { + + var getRandomNumber = function(range) { + return Math.floor(Math.random() * range); + }; + + var getRandomChar = function() { + var chars = "abcdefghijklmnopqurstuvwxyzABCDEFGHIJKLMNOPQURSTUVWXYZ"; + return chars.substr( getRandomNumber(62), 1 ); + }; + + var randomID = function(size) { + var str = ""; + for(var i = 0; i < size; i++) { + str += getRandomChar(); + } + return str; + }; + + return randomID(size); + }, + /* * Tells you if a number is even or not + ================================================== */ + // VMM.Util.isEven(n) + isEven: function(n){ + return (n%2 === 0) ? true : false; + }, + /* * Get URL Variables + ================================================== */ + // var somestring = VMM.Util.getUrlVars(str_url)["varname"]; + getUrlVars: function(string) { + + var str = string.toString(); + + if (str.match('&')) { + str = str.replace("&", "&"); + } else if (str.match('&')) { + str = str.replace("&", "&"); + } else if (str.match('&')) { + str = str.replace("&", "&"); + } + + var vars = [], hash; + var hashes = str.slice(str.indexOf('?') + 1).split('&'); + for(var i = 0; i < hashes.length; i++) { + hash = hashes[i].split('='); + vars.push(hash[0]); + vars[hash[0]] = hash[1]; + } + + + return vars; + }, + + /* * Cleans up strings to become real HTML + ================================================== */ + toHTML: function(text) { + + text = this.nl2br(text); + text = this.linkify(text); + + return text.replace(/\s\s/g,"  "); + }, + + /* * Returns text strings as CamelCase + ================================================== */ + toCamelCase: function(s,forceLowerCase) { + + if(forceLowerCase !== false) forceLowerCase = true; + + var sps = ((forceLowerCase) ? s.toLowerCase() : s).split(" "); + + for(var i=0; i 1 ? '.' + x[1] : ''; + var rgx = /(\d+)(\d{3})/; + while (rgx.test(x1)) { + x1 = x1.replace(rgx, '$1' + ',' + '$2'); + } + return x1 + x2; + }, + /* * Transform text to Title Case + ================================================== */ + toTitleCase: function(t){ + if ( VMM.Browser.browser == "Explorer" && parseInt(VMM.Browser.version, 10) >= 7) { + return t.replace("_", "%20"); + } else { + var __TitleCase = { + __smallWords: ['a', 'an', 'and', 'as', 'at', 'but','by', 'en', 'for', 'if', 'in', 'of', 'on', 'or','the', 'to', 'v[.]?', 'via', 'vs[.]?'], + + init: function() { + this.__smallRE = this.__smallWords.join('|'); + this.__lowerCaseWordsRE = new RegExp('\\b(' + this.__smallRE + ')\\b', 'gi'); + this.__firstWordRE = new RegExp('^([^a-zA-Z0-9 \\r\\n\\t]*)(' + this.__smallRE + ')\\b', 'gi'); + this.__lastWordRE = new RegExp('\\b(' + this.__smallRE + ')([^a-zA-Z0-9 \\r\\n\\t]*)$', 'gi'); + }, + + toTitleCase: function(string) { + var line = ''; + + var split = string.split(/([:.;?!][ ]|(?:[ ]|^)["“])/); + + for (var i = 0; i < split.length; ++i) { + var s = split[i]; + + s = s.replace(/\b([a-zA-Z][a-z.'’]*)\b/g,this.__titleCaseDottedWordReplacer); + + // lowercase the list of small words + s = s.replace(this.__lowerCaseWordsRE, this.__lowerReplacer); + + // if the first word in the title is a small word then capitalize it + s = s.replace(this.__firstWordRE, this.__firstToUpperCase); + + // if the last word in the title is a small word, then capitalize it + s = s.replace(this.__lastWordRE, this.__firstToUpperCase); + + line += s; + } + + // special cases + line = line.replace(/ V(s?)\. /g, ' v$1. '); + line = line.replace(/(['’])S\b/g, '$1s'); + line = line.replace(/\b(AT&T|Q&A)\b/ig, this.__upperReplacer); + + return line; + }, + + __titleCaseDottedWordReplacer: function (w) { + return (w.match(/[a-zA-Z][.][a-zA-Z]/)) ? w : __TitleCase.__firstToUpperCase(w); + }, + + __lowerReplacer: function (w) { return w.toLowerCase() }, + + __upperReplacer: function (w) { return w.toUpperCase() }, + + __firstToUpperCase: function (w) { + var split = w.split(/(^[^a-zA-Z0-9]*[a-zA-Z0-9])(.*)$/); + if (split[1]) { + split[1] = split[1].toUpperCase(); + } + + return split.join(''); + + + } + }; + + __TitleCase.init(); + + t = t.replace(/_/g," "); + t = __TitleCase.toTitleCase(t); + + return t; + + } + + } + + }).init(); +} + +/* ********************************************** + Begin LazyLoad.js +********************************************** */ + +/*jslint browser: true, eqeqeq: true, bitwise: true, newcap: true, immed: true, regexp: false */ + +/* +LazyLoad makes it easy and painless to lazily load one or more external +JavaScript or CSS files on demand either during or after the rendering of a web +page. + +Supported browsers include Firefox 2+, IE6+, Safari 3+ (including Mobile +Safari), Google Chrome, and Opera 9+. Other browsers may or may not work and +are not officially supported. + +Visit https://github.com/rgrove/lazyload/ for more info. + +Copyright (c) 2011 Ryan Grove +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +@module lazyload +@class LazyLoad +@static +@version 2.0.3 (git) +*/ + +LazyLoad = (function (doc) { + // -- Private Variables ------------------------------------------------------ + + // User agent and feature test information. + var env, + + // Reference to the element (populated lazily). + head, + + // Requests currently in progress, if any. + pending = {}, + + // Number of times we've polled to check whether a pending stylesheet has + // finished loading. If this gets too high, we're probably stalled. + pollCount = 0, + + // Queued requests. + queue = {css: [], js: []}, + + // Reference to the browser's list of stylesheets. + styleSheets = doc.styleSheets; + + // -- Private Methods -------------------------------------------------------- + + /** + Creates and returns an HTML element with the specified name and attributes. + + @method createNode + @param {String} name element name + @param {Object} attrs name/value mapping of element attributes + @return {HTMLElement} + @private + */ + function createNode(name, attrs) { + var node = doc.createElement(name), attr; + + for (attr in attrs) { + if (attrs.hasOwnProperty(attr)) { + node.setAttribute(attr, attrs[attr]); + } + } + + return node; + } + + /** + Called when the current pending resource of the specified type has finished + loading. Executes the associated callback (if any) and loads the next + resource in the queue. + + @method finish + @param {String} type resource type ('css' or 'js') + @private + */ + function finish(type) { + var p = pending[type], + callback, + urls; + + if (p) { + callback = p.callback; + urls = p.urls; + + urls.shift(); + pollCount = 0; + + // If this is the last of the pending URLs, execute the callback and + // start the next request in the queue (if any). + if (!urls.length) { + callback && callback.call(p.context, p.obj); + pending[type] = null; + queue[type].length && load(type); + } + } + } + + /** + Populates the env variable with user agent and feature test + information. + + @method getEnv + @private + */ + function getEnv() { + var ua = navigator.userAgent; + + env = { + // True if this browser supports disabling async mode on dynamically + // created script nodes. See + // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order + async: doc.createElement('script').async === true + }; + + (env.webkit = /AppleWebKit\//.test(ua)) + || (env.ie = /MSIE/.test(ua)) + || (env.opera = /Opera/.test(ua)) + || (env.gecko = /Gecko\//.test(ua)) + || (env.unknown = true); + } + + /** + Loads the specified resources, or the next resource of the specified type + in the queue if no resources are specified. If a resource of the specified + type is already being loaded, the new request will be queued until the + first request has been finished. + + When an array of resource URLs is specified, those URLs will be loaded in + parallel if it is possible to do so while preserving execution order. All + browsers support parallel loading of CSS, but only Firefox and Opera + support parallel loading of scripts. In other browsers, scripts will be + queued and loaded one at a time to ensure correct execution order. + + @method load + @param {String} type resource type ('css' or 'js') + @param {String|Array} urls (optional) URL or array of URLs to load + @param {Function} callback (optional) callback function to execute when the + resource is loaded + @param {Object} obj (optional) object to pass to the callback function + @param {Object} context (optional) if provided, the callback function will + be executed in this object's context + @private + */ + function load(type, urls, callback, obj, context) { + var _finish = function () { finish(type); }, + isCSS = type === 'css', + nodes = [], + i, len, node, p, pendingUrls, url; + + env || getEnv(); + + if (urls) { + // If urls is a string, wrap it in an array. Otherwise assume it's an + // array and create a copy of it so modifications won't be made to the + // original. + urls = typeof urls === 'string' ? [urls] : urls.concat(); + + // Create a request object for each URL. If multiple URLs are specified, + // the callback will only be executed after all URLs have been loaded. + // + // Sadly, Firefox and Opera are the only browsers capable of loading + // scripts in parallel while preserving execution order. In all other + // browsers, scripts must be loaded sequentially. + // + // All browsers respect CSS specificity based on the order of the link + // elements in the DOM, regardless of the order in which the stylesheets + // are actually downloaded. + if (isCSS || env.async || env.gecko || env.opera) { + // Load in parallel. + queue[type].push({ + urls : urls, + callback: callback, + obj : obj, + context : context + }); + } else { + // Load sequentially. + for (i = 0, len = urls.length; i < len; ++i) { + queue[type].push({ + urls : [urls[i]], + callback: i === len - 1 ? callback : null, // callback is only added to the last URL + obj : obj, + context : context + }); + } + } + } + + // If a previous load request of this type is currently in progress, we'll + // wait our turn. Otherwise, grab the next item in the queue. + if (pending[type] || !(p = pending[type] = queue[type].shift())) { + return; + } + + head || (head = doc.head || doc.getElementsByTagName('head')[0]); + pendingUrls = p.urls; + + for (i = 0, len = pendingUrls.length; i < len; ++i) { + url = pendingUrls[i]; + + if (isCSS) { + node = env.gecko ? createNode('style') : createNode('link', { + href: url, + rel : 'stylesheet' + }); + } else { + node = createNode('script', {src: url}); + node.async = false; + } + + node.className = 'lazyload'; + node.setAttribute('charset', 'utf-8'); + + if (env.ie && !isCSS) { + node.onreadystatechange = function () { + if (/loaded|complete/.test(node.readyState)) { + node.onreadystatechange = null; + _finish(); + } + }; + } else if (isCSS && (env.gecko || env.webkit)) { + // Gecko and WebKit don't support the onload event on link nodes. + if (env.webkit) { + // In WebKit, we can poll for changes to document.styleSheets to + // figure out when stylesheets have loaded. + p.urls[i] = node.href; // resolve relative URLs (or polling won't work) + pollWebKit(); + } else { + // In Gecko, we can import the requested URL into a diff --git a/test/view.timeline.test.js b/test/view.timeline.test.js index d76d52e8..b57b8757 100644 --- a/test/view.timeline.test.js +++ b/test/view.timeline.test.js @@ -43,11 +43,10 @@ test('render etc', function () { }); $('.fixtures').append(view.el); view.render(); - view._initTimeline(); assertPresent('.vco-timeline', view.el); assertPresent('.timenav', view.el); assertPresent('.timenav', view.el); - equal(view.$el.find('.marker.active h3').text(), 'firstfirst'); + equal(view.$el.find('.marker.active h3').text(), 'first'); view.remove(); }); From 2adc2138ef5060fecd41e05968149b4cbcbae9fd Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 24 Aug 2013 13:14:07 +0100 Subject: [PATCH 57/74] [#316,timeline][s]: upgrade timelinejs vendor lib to v2.25 for better date extents (need to patch again to get working). --- vendor/timeline/{2.24 => 2.25}/LICENSE | 0 .../timeline/{2.24 => 2.25}/css/loading.gif | Bin .../timeline/{2.24 => 2.25}/css/timeline.css | 0 .../timeline/{2.24 => 2.25}/css/timeline.png | Bin .../{2.24 => 2.25}/css/timeline@2x.png | Bin vendor/timeline/{2.24 => 2.25}/js/timeline.js | 30 ++++++++++++++---- 6 files changed, 23 insertions(+), 7 deletions(-) rename vendor/timeline/{2.24 => 2.25}/LICENSE (100%) rename vendor/timeline/{2.24 => 2.25}/css/loading.gif (100%) rename vendor/timeline/{2.24 => 2.25}/css/timeline.css (100%) rename vendor/timeline/{2.24 => 2.25}/css/timeline.png (100%) rename vendor/timeline/{2.24 => 2.25}/css/timeline@2x.png (100%) rename vendor/timeline/{2.24 => 2.25}/js/timeline.js (99%) diff --git a/vendor/timeline/2.24/LICENSE b/vendor/timeline/2.25/LICENSE similarity index 100% rename from vendor/timeline/2.24/LICENSE rename to vendor/timeline/2.25/LICENSE diff --git a/vendor/timeline/2.24/css/loading.gif b/vendor/timeline/2.25/css/loading.gif similarity index 100% rename from vendor/timeline/2.24/css/loading.gif rename to vendor/timeline/2.25/css/loading.gif diff --git a/vendor/timeline/2.24/css/timeline.css b/vendor/timeline/2.25/css/timeline.css similarity index 100% rename from vendor/timeline/2.24/css/timeline.css rename to vendor/timeline/2.25/css/timeline.css diff --git a/vendor/timeline/2.24/css/timeline.png b/vendor/timeline/2.25/css/timeline.png similarity index 100% rename from vendor/timeline/2.24/css/timeline.png rename to vendor/timeline/2.25/css/timeline.png diff --git a/vendor/timeline/2.24/css/timeline@2x.png b/vendor/timeline/2.25/css/timeline@2x.png similarity index 100% rename from vendor/timeline/2.24/css/timeline@2x.png rename to vendor/timeline/2.25/css/timeline@2x.png diff --git a/vendor/timeline/2.24/js/timeline.js b/vendor/timeline/2.25/js/timeline.js similarity index 99% rename from vendor/timeline/2.24/js/timeline.js rename to vendor/timeline/2.25/js/timeline.js index 4ac28981..a64dea2b 100644 --- a/vendor/timeline/2.24/js/timeline.js +++ b/vendor/timeline/2.25/js/timeline.js @@ -1277,6 +1277,7 @@ if(typeof VMM != 'undefined' && typeof VMM.Date == 'undefined') { }; if (type.of(d) == "date") { + trace("DEBUG THIS, ITs A DATE"); date = d; } else { date = new Date(0, 0, 1, 0, 0, 0, 0); @@ -1398,7 +1399,7 @@ if(typeof VMM != 'undefined' && typeof VMM.Date == 'undefined') { p.second = true; p.millisecond = true; } - } else if (d.length <= 5) { + } else if (d.length <= 8) { p.year = true; date.setFullYear(parseInt(d, 10)); date.setMonth(0); @@ -1446,6 +1447,13 @@ if(typeof VMM != 'undefined' && typeof VMM.Date == 'undefined') { } else { date = new Date(Date.parse(d)); + p.year = true; + p.month = true; + p.day = true; + p.hour = true; + p.minute = true; + p.second = true; + p.millisecond = true; } } else { p.year = true; @@ -2899,6 +2907,11 @@ if(typeof VMM != 'undefined' && typeof VMM.ExternalAPI == 'undefined') { }, + errorTimeOutOembed: function(tweet) { + trace("TWITTER JSON ERROR TIMEOUT " + tweet.mid); + VMM.attachElement("#"+tweet.id.toString(), VMM.MediaElement.loadingmessage("Still waiting on Twitter: " + tweet.mid) ); + }, + pushQue: function() { if (VMM.master_config.twitter.que.length > 0) { VMM.ExternalAPI.twitter.create(VMM.master_config.twitter.que[0], VMM.ExternalAPI.twitter.pushQue); @@ -2908,7 +2921,9 @@ if(typeof VMM != 'undefined' && typeof VMM.ExternalAPI == 'undefined') { getOEmbed: function(tweet, callback) { - var the_url = "http://api.twitter.com/1/statuses/oembed.json?id=" + tweet.mid + "&omit_script=true&include_entities=true&callback=?"; + var the_url = "http://api.twitter.com/1/statuses/oembed.json?id=" + tweet.mid + "&omit_script=true&include_entities=true&callback=?", + twitter_timeout = setTimeout(VMM.ExternalAPI.twitter.errorTimeOutOembed, VMM.master_config.timers.api, tweet); + //callback_timeout= setTimeout(callback, VMM.master_config.timers.api, tweet); VMM.getJSON(the_url, function(d) { var twit = "", @@ -2936,9 +2951,13 @@ if(typeof VMM != 'undefined' && typeof VMM.ExternalAPI == 'undefined') { .error(function(jqXHR, textStatus, errorThrown) { trace("TWITTER error"); trace("TWITTER ERROR: " + textStatus + " " + jqXHR.responseText); + clearTimeout(twitter_timeout); + //clearTimeout(callback_timeout); VMM.attachElement("#"+tweet.id, VMM.MediaElement.loadingmessage("ERROR LOADING TWEET " + tweet.mid) ); }) .success(function(d) { + clearTimeout(twitter_timeout); + clearTimeout(callback_timeout); callback(); }); @@ -7281,10 +7300,7 @@ if(typeof VMM != 'undefined' && typeof VMM.Timeline == 'undefined') { createConfig(c); createStructure(); - // surely this should just be type.of(_data) !== null - // if (type.of(_data) == "string") { - // OR - should not use _data as an argument - if (type.of(_data) != null) { + if (type.of(_data) == "string") { config.source = _data; } @@ -9990,4 +10006,4 @@ if (typeof VMM.Timeline !== 'undefined' && typeof VMM.Timeline.DataObj == 'undef }; -} +} \ No newline at end of file From fddea8e882a713dd04e8591478e9980480b85f88 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 24 Aug 2013 13:16:45 +0100 Subject: [PATCH 58/74] [#316,refactor][xs]: move vendor timeline stuff to vendor/timeline w/o versions to ease upgrading. --- vendor/timeline/{2.25 => }/LICENSE | 0 vendor/timeline/README | 1 + vendor/timeline/{2.25 => }/css/loading.gif | Bin vendor/timeline/{2.25 => }/css/timeline.css | 0 vendor/timeline/{2.25 => }/css/timeline.png | Bin vendor/timeline/{2.25 => }/css/timeline@2x.png | Bin vendor/timeline/{2.25 => }/js/timeline.js | 0 7 files changed, 1 insertion(+) rename vendor/timeline/{2.25 => }/LICENSE (100%) create mode 100644 vendor/timeline/README rename vendor/timeline/{2.25 => }/css/loading.gif (100%) rename vendor/timeline/{2.25 => }/css/timeline.css (100%) rename vendor/timeline/{2.25 => }/css/timeline.png (100%) rename vendor/timeline/{2.25 => }/css/timeline@2x.png (100%) rename vendor/timeline/{2.25 => }/js/timeline.js (100%) diff --git a/vendor/timeline/2.25/LICENSE b/vendor/timeline/LICENSE similarity index 100% rename from vendor/timeline/2.25/LICENSE rename to vendor/timeline/LICENSE diff --git a/vendor/timeline/README b/vendor/timeline/README new file mode 100644 index 00000000..50f93da3 --- /dev/null +++ b/vendor/timeline/README @@ -0,0 +1 @@ +Verite TimelineJS v2.25 diff --git a/vendor/timeline/2.25/css/loading.gif b/vendor/timeline/css/loading.gif similarity index 100% rename from vendor/timeline/2.25/css/loading.gif rename to vendor/timeline/css/loading.gif diff --git a/vendor/timeline/2.25/css/timeline.css b/vendor/timeline/css/timeline.css similarity index 100% rename from vendor/timeline/2.25/css/timeline.css rename to vendor/timeline/css/timeline.css diff --git a/vendor/timeline/2.25/css/timeline.png b/vendor/timeline/css/timeline.png similarity index 100% rename from vendor/timeline/2.25/css/timeline.png rename to vendor/timeline/css/timeline.png diff --git a/vendor/timeline/2.25/css/timeline@2x.png b/vendor/timeline/css/timeline@2x.png similarity index 100% rename from vendor/timeline/2.25/css/timeline@2x.png rename to vendor/timeline/css/timeline@2x.png diff --git a/vendor/timeline/2.25/js/timeline.js b/vendor/timeline/js/timeline.js similarity index 100% rename from vendor/timeline/2.25/js/timeline.js rename to vendor/timeline/js/timeline.js From c8f7ab56ff42bb66580942d1c8040324189e1381 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sat, 24 Aug 2013 13:25:28 +0100 Subject: [PATCH 59/74] [#316,timeline][s]: complete upgrade to v2.25 of timelinejs with support for BC dates <= 10k BC. --- _includes/recline-deps.html | 4 ++-- docs/tutorial-views.markdown | 4 ++-- test/index.html | 4 ++-- test/view.timeline.test.js | 9 ++++++++- vendor/timeline/js/timeline.js | 10 ++++++++-- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/_includes/recline-deps.html b/_includes/recline-deps.html index ca6de2ce..2f4b241a 100644 --- a/_includes/recline-deps.html +++ b/_includes/recline-deps.html @@ -8,7 +8,7 @@ - + @@ -36,7 +36,7 @@ - + diff --git a/docs/tutorial-views.markdown b/docs/tutorial-views.markdown index 3aab9627..f8d4a916 100644 --- a/docs/tutorial-views.markdown +++ b/docs/tutorial-views.markdown @@ -248,11 +248,11 @@ First, add the additional dependencies for the timeline view. The timeline is bu {% highlight html %} - + - + {% endhighlight %} Now, create a new div for the map (must have an explicit height for the timeline to render): diff --git a/test/index.html b/test/index.html index 34f9b026..1dc22ed8 100644 --- a/test/index.html +++ b/test/index.html @@ -4,7 +4,7 @@ Qunit Tests - + @@ -26,7 +26,7 @@ - + diff --git a/test/view.timeline.test.js b/test/view.timeline.test.js index b57b8757..12850279 100644 --- a/test/view.timeline.test.js +++ b/test/view.timeline.test.js @@ -37,7 +37,13 @@ test('extract dates and timelineJSON', function () { }); test('render etc', function () { - var dataset = Fixture.getDataset(); + var dataset = new recline.Model.Dataset({ + records: [ + {'Date': '-10000', 'title': 'first'}, + {'Date': '2012-03-20', 'title': 'second'}, + {'Date': '2012-03-25', 'title': 'third'} + ] + }); var view = new recline.View.Timeline({ model: dataset }); @@ -63,6 +69,7 @@ test('_parseDate', function () { [ '1914-08-01T08:00', '1914,08,01,08,00' ], [ '03-20-1914', '03/20/1914' ], [ '20/03/1914', '20/03/1914' ], + [ '-10000', '-10000' ], [ null, null ] ]; _.each(testData, function(item) { diff --git a/vendor/timeline/js/timeline.js b/vendor/timeline/js/timeline.js index a64dea2b..4c7745aa 100644 --- a/vendor/timeline/js/timeline.js +++ b/vendor/timeline/js/timeline.js @@ -7300,7 +7300,13 @@ if(typeof VMM != 'undefined' && typeof VMM.Timeline == 'undefined') { createConfig(c); createStructure(); - if (type.of(_data) == "string") { + // FIX + // Current is + // if (type.of(_data) == "string") { + // BUT surely should just be + // type.of(_data) !== null + // OR - should not allow _data as an argument + if (type.of(_data) !== null) { config.source = _data; } @@ -10006,4 +10012,4 @@ if (typeof VMM.Timeline !== 'undefined' && typeof VMM.Timeline.DataObj == 'undef }; -} \ No newline at end of file +} From ef1064b8881659cd4f038eea7f5d80eb989ce45f Mon Sep 17 00:00:00 2001 From: kielni Date: Fri, 6 Sep 2013 11:40:31 -0700 Subject: [PATCH 60/74] add method to replace a filter --- src/model.js | 17 +++++++++++++++++ test/model.test.js | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/model.js b/src/model.js index 5649b1d8..c18aa2fb 100644 --- a/src/model.js +++ b/src/model.js @@ -510,6 +510,23 @@ my.Query = Backbone.Model.extend({ filters.push(ourfilter); this.trigger('change:filters:new-blank'); }, + replaceFilter: function(filter) { + // delete filter on the same field, then add + var filters = this.get('filters'); + var idx = -1; + _.each(this.get('filters'), function(filter, key, list) { + if (filter.field == filter.field) { + idx = key; + } + }); + // trigger just one event (change:filters:new-blank) instead of one for remove and + // one for add + if (idx >= 0) { + filters.splice(idx, 1); + this.set({filters: filters}); + } + this.addFilter(filter); + }, updateFilter: function(index, value) { }, // ### removeFilter diff --git a/test/model.test.js b/test/model.test.js index 00797ef0..506ef2bd 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -374,4 +374,24 @@ test('Query.addFilter', function () { deepEqual(exp, query.get('filters')[2]); }); +test('Query.replaceFilter', function () { + var query = new recline.Model.Query(); + query.addFilter({type: 'term', field: 'xyz'}); + var exp = { + field: 'xyz', + type: 'term', + term: '' + }; + deepEqual(query.get('filters')[0], exp); + + query.replaceFilter({type: 'term', field: 'abc'}); + exp = { + field: 'abc', + type: 'term', + term: '' + }; + deepEqual(query.get('filters')[0], exp); + +}); + })(this.jQuery); From 95fab060c3a989bb7b60079918d6dfcc0a9f4e46 Mon Sep 17 00:00:00 2001 From: kielni Date: Fri, 6 Sep 2013 11:58:23 -0700 Subject: [PATCH 61/74] add options to addFacet for size and silent --- src/model.js | 9 +++++++-- test/model.test.js | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/model.js b/src/model.js index c18aa2fb..d72bfa8d 100644 --- a/src/model.js +++ b/src/model.js @@ -543,7 +543,7 @@ my.Query = Backbone.Model.extend({ // Add a Facet to this query // // See - addFacet: function(fieldId) { + addFacet: function(fieldId, size, silent) { var facets = this.get('facets'); // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) if (_.contains(_.keys(facets), fieldId)) { @@ -552,8 +552,13 @@ my.Query = Backbone.Model.extend({ facets[fieldId] = { terms: { field: fieldId } }; + if (!_.isUndefined(size)) { + facets[fieldId].terms.size = size; + } this.set({facets: facets}, {silent: true}); - this.trigger('facet:add', this); + if (!silent) { + this.trigger('facet:add', this); + } }, addHistogramFacet: function(fieldId) { var facets = this.get('facets'); diff --git a/test/model.test.js b/test/model.test.js index 506ef2bd..b1ed0744 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -347,6 +347,12 @@ test('Query', function () { deepEqual({terms: {field: 'xyz'}}, query.get('facets')['xyz']); }); +test('Query.addFacet', function () { + var query = new recline.Model.Query(); + query.addFacet('xyz', 25); + deepEqual({terms: {field: 'xyz', "size": 25}}, query.get('facets')['xyz']); +}); + test('Query.addFilter', function () { var query = new recline.Model.Query(); query.addFilter({type: 'term', field: 'xyz'}); From f7f010ea33186e5c603e9927956dec65b802c5e5 Mon Sep 17 00:00:00 2001 From: kielni Date: Fri, 6 Sep 2013 13:51:17 -0700 Subject: [PATCH 62/74] add new methods for facets: removeFacet, clearFacets, and refreshFacets --- src/model.js | 23 +++++++++++++++++++++++ test/model.test.js | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/model.js b/src/model.js index d72bfa8d..721a0c42 100644 --- a/src/model.js +++ b/src/model.js @@ -570,7 +570,30 @@ my.Query = Backbone.Model.extend({ }; this.set({facets: facets}, {silent: true}); this.trigger('facet:add', this); + }, + removeFacet: function(fieldId) { + var facets = this.get('facets'); + // Assume id and fieldId should be the same (TODO: this need not be true if we want to add two different type of facets on same field) + if (!_.contains(_.keys(facets), fieldId)) { + return; + } + delete facets[fieldId]; + this.set({facets: facets}, {silent: true}); + this.trigger('facet:remove', this); + }, + clearFacets: function() { + var facets = this.get('facets'); + _.each(_.keys(facets), function(fieldId) { + delete facets[fieldId]; + }); + this.trigger('facet:remove', this); + }, + // trigger a facet add; use this to trigger a single event after adding + // multiple facets + refreshFacets: function() { + this.trigger('facet:add', this); } + }); diff --git a/test/model.test.js b/test/model.test.js index b1ed0744..e052a391 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -353,6 +353,24 @@ test('Query.addFacet', function () { deepEqual({terms: {field: 'xyz', "size": 25}}, query.get('facets')['xyz']); }); +test('Query.removeFacet', function () { + var query = new recline.Model.Query(); + query.addFacet('xyz'); + deepEqual({terms: {field: 'xyz'}}, query.get('facets')['xyz']); + query.removeFacet('xyz'); + equal(undefined, query.get('facets')['xyz']); +}); + +test('Query.clearFacets', function () { + var query = new recline.Model.Query(); + query.addFacet('abc'); + query.addFacet('xyz'); + deepEqual({terms: {field: 'xyz'}}, query.get('facets')['xyz']); + deepEqual({terms: {field: 'abc'}}, query.get('facets')['abc']); + query.clearFacets(); + deepEqual({}, query.get('facets')); +}); + test('Query.addFilter', function () { var query = new recline.Model.Query(); query.addFilter({type: 'term', field: 'xyz'}); From 91c0f7704a746bac0e8e24c6b658754fcddd1496 Mon Sep 17 00:00:00 2001 From: kielni Date: Mon, 9 Sep 2013 07:31:12 -0700 Subject: [PATCH 63/74] allow a dataset to override handleQueryResult --- src/model.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/model.js b/src/model.js index 721a0c42..60382a06 100644 --- a/src/model.js +++ b/src/model.js @@ -46,6 +46,10 @@ my.Dataset = Backbone.Model.extend({ if (this.backend == recline.Backend.Memory) { this.fetch(); } + + // if backend has a handleQueryResultFunction, use that + this._handleResult = (_.has(this.backend, 'handleQueryResult')) ? + this.backend.handleQueryResult : this._handleQueryResult; }, // ### fetch @@ -189,7 +193,7 @@ my.Dataset = Backbone.Model.extend({ this._store.query(actualQuery, this.toJSON()) .done(function(queryResult) { - self._handleQueryResult(queryResult); + self._handleResult(queryResult); self.trigger('query:done'); dfd.resolve(self.records); }) From ee2067da684840eefc5ea6c965e77269a458c617 Mon Sep 17 00:00:00 2001 From: kielni Date: Tue, 10 Sep 2013 10:21:02 -0700 Subject: [PATCH 64/74] this.backend might be null; set handleResult before fetch --- src/model.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/model.js b/src/model.js index 60382a06..be9ca3e9 100644 --- a/src/model.js +++ b/src/model.js @@ -43,13 +43,13 @@ my.Dataset = Backbone.Model.extend({ // store will either be the backend or be a memory store if Backend fetch // tells us to use memory store this._store = this.backend; + + // if backend has a handleQueryResultFunction, use that + this._handleResult = (this.backend != null && _.has(this.backend, 'handleQueryResult')) ? + this.backend.handleQueryResult : this._handleQueryResult; if (this.backend == recline.Backend.Memory) { this.fetch(); } - - // if backend has a handleQueryResultFunction, use that - this._handleResult = (_.has(this.backend, 'handleQueryResult')) ? - this.backend.handleQueryResult : this._handleQueryResult; }, // ### fetch From b550b2801b597d929aaf761ca5e72d4a5d295abf Mon Sep 17 00:00:00 2001 From: kielni Date: Fri, 25 Oct 2013 10:17:41 -0700 Subject: [PATCH 65/74] make backend memory store work with from/to filters --- dist/recline.js | 10 +++++---- src/backend.memory.js | 10 +++++---- test/backend.memory.test.js | 45 +++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/dist/recline.js b/dist/recline.js index da4b6b53..d95cbb53 100644 --- a/dist/recline.js +++ b/dist/recline.js @@ -524,12 +524,14 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var startnull = (filter.start === null || filter.start === ''); - var stopnull = (filter.stop === null || filter.stop === ''); + var filterStart = filter.start || filter.from; + var filterStop = filter.stop || filter.to; + var startnull = (_.isUndefined(filterStart) || filterStart === null || filterStart === ''); + var stopnull = (_.isUndefined(filterStop) || filterStop === null || filterStop === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); - var start = parse(filter.start); - var stop = parse(filter.stop); + var start = parse(startnull ? '' : filterStart); + var stop = parse(stopnull ? '' : filterStop); // if at least one end of range is set do not allow '' to get through // note that for strings '' <= {any-character} e.g. '' <= 'a' diff --git a/src/backend.memory.js b/src/backend.memory.js index 0e6094cc..00f1db4c 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -141,12 +141,14 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var startnull = (filter.start === null || filter.start === ''); - var stopnull = (filter.stop === null || filter.stop === ''); + var filterStart = filter.start || filter.from; + var filterStop = filter.stop || filter.to; + var startnull = (_.isUndefined(filterStart) || filterStart === null || filterStart === ''); + var stopnull = (_.isUndefined(filterStop) || filterStop === null || filterStop === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); - var start = parse(filter.start); - var stop = parse(filter.stop); + var start = parse(startnull ? '' : filterStart); + var stop = parse(stopnull ? '' : filterStop); // if at least one end of range is set do not allow '' to get through // note that for strings '' <= {any-character} e.g. '' <= 'a' diff --git a/test/backend.memory.test.js b/test/backend.memory.test.js index e4636884..9b88bec2 100644 --- a/test/backend.memory.test.js +++ b/test/backend.memory.test.js @@ -112,6 +112,20 @@ test('filters', function () { equal(out.total, 3); deepEqual(_.pluck(out.hits, 'z'), [3,6,9]); }); + + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'date', from: '2011-01-01', to: '2011-05-01'}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 3); + deepEqual(_.pluck(out.hits, 'date'), ['2011-01-01','2011-02-03','2011-04-05']); + }); + + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'z', from: '0', to: '10'}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 3); + deepEqual(_.pluck(out.hits, 'z'), [3,6,9]); + }); }); @@ -124,18 +138,36 @@ test('filters with nulls', function () { equal(out.total, 6); }); + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'z', from: '', to: null}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 6); + }); + query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', start: '', stop: '3'}); data.query(query.toJSON()).then(function(out) { equal(out.total, 3); }); + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'x', from: '', to: '3'}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 3); + }); + query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', start: '3', stop: ''}); data.query(query.toJSON()).then(function(out) { equal(out.total, 4); }); + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'x', from: '3', to: ''}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 4); + }); + data.records[5].country = ''; query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'country', start: '', stop: 'Z'}); @@ -143,11 +175,24 @@ test('filters with nulls', function () { equal(out.total, 5); }); + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'country', from: '', to: 'Z'}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 5); + }); + query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', start: '', stop: ''}); data.query(query.toJSON()).then(function(out) { equal(out.total, 6); }); + + query = new recline.Model.Query(); + query.addFilter({type: 'range', field: 'x', from: '', to: ''}); + data.query(query.toJSON()).then(function(out) { + equal(out.total, 6); + }); + }); test('facet', function () { From 56ac25611e477db225b88d4f84ab3483e62ac536 Mon Sep 17 00:00:00 2001 From: kielni Date: Tue, 29 Oct 2013 07:29:51 -0700 Subject: [PATCH 66/74] change range filter template to match Elasticsearch convention: from/to instead of start/stop --- src/model.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/model.js b/src/model.js index be9ca3e9..a70ba1ac 100644 --- a/src/model.js +++ b/src/model.js @@ -485,8 +485,8 @@ my.Query = Backbone.Model.extend({ }, range: { type: 'range', - start: '', - stop: '' + from: '', + to: '' }, geo_distance: { type: 'geo_distance', From 03c3afbb51093d56f5f634f0aa5c3eae1bb40e0b Mon Sep 17 00:00:00 2001 From: kielni Date: Tue, 29 Oct 2013 12:17:58 -0700 Subject: [PATCH 67/74] update filtereditor to use from/to instead of start/stop --- src/backend.memory.js | 14 ++++------ src/widget.filtereditor.js | 4 +-- test/backend.memory.test.js | 48 ++------------------------------ test/widget.filtereditor.test.js | 2 +- 4 files changed, 12 insertions(+), 56 deletions(-) diff --git a/src/backend.memory.js b/src/backend.memory.js index 00f1db4c..0c83a7d1 100644 --- a/src/backend.memory.js +++ b/src/backend.memory.js @@ -141,21 +141,19 @@ this.recline.Backend.Memory = this.recline.Backend.Memory || {}; } function range(record, filter) { - var filterStart = filter.start || filter.from; - var filterStop = filter.stop || filter.to; - var startnull = (_.isUndefined(filterStart) || filterStart === null || filterStart === ''); - var stopnull = (_.isUndefined(filterStop) || filterStop === null || filterStop === ''); + var fromnull = (_.isUndefined(filter.from) || filter.from === null || filter.from === ''); + var tonull = (_.isUndefined(filter.to) || filter.to === null || filter.to === ''); var parse = getDataParser(filter); var value = parse(record[filter.field]); - var start = parse(startnull ? '' : filterStart); - var stop = parse(stopnull ? '' : filterStop); + var from = parse(fromnull ? '' : filter.from); + var to = parse(tonull ? '' : filter.to); // if at least one end of range is set do not allow '' to get through // note that for strings '' <= {any-character} e.g. '' <= 'a' - if ((!startnull || !stopnull) && value === '') { + if ((!fromnull || !tonull) && value === '') { return false; } - return ((startnull || value >= start) && (stopnull || value <= stop)); + return ((fromnull || value >= from) && (tonull || value <= to)); } function geo_distance() { diff --git a/src/widget.filtereditor.js b/src/widget.filtereditor.js index ff92af87..3473b9d5 100644 --- a/src/widget.filtereditor.js +++ b/src/widget.filtereditor.js @@ -59,9 +59,9 @@ my.FilterEditor = Backbone.View.extend({ × \ \ \ - \ + \ \ - \ + \ \
    \ ', diff --git a/test/backend.memory.test.js b/test/backend.memory.test.js index 9b88bec2..440e7d8d 100644 --- a/test/backend.memory.test.js +++ b/test/backend.memory.test.js @@ -99,20 +99,6 @@ test('filters', function () { deepEqual(_.pluck(out.hits, 'country'), ['UK','UK','UK']); }); - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'date', start: '2011-01-01', stop: '2011-05-01'}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 3); - deepEqual(_.pluck(out.hits, 'date'), ['2011-01-01','2011-02-03','2011-04-05']); - }); - - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'z', start: '0', stop: '10'}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 3); - deepEqual(_.pluck(out.hits, 'z'), [3,6,9]); - }); - query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'date', from: '2011-01-01', to: '2011-05-01'}); data.query(query.toJSON()).then(function(out) { @@ -126,42 +112,25 @@ test('filters', function () { equal(out.total, 3); deepEqual(_.pluck(out.hits, 'z'), [3,6,9]); }); + }); test('filters with nulls', function () { var data = _wrapData(); - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'z', start: '', stop: null}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 6); - }); - query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'z', from: '', to: null}); data.query(query.toJSON()).then(function(out) { equal(out.total, 6); }); - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'x', start: '', stop: '3'}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 3); - }); - query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', from: '', to: '3'}); data.query(query.toJSON()).then(function(out) { equal(out.total, 3); }); - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'x', start: '3', stop: ''}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 4); - }); - query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', from: '3', to: ''}); data.query(query.toJSON()).then(function(out) { @@ -169,11 +138,6 @@ test('filters with nulls', function () { }); data.records[5].country = ''; - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'country', start: '', stop: 'Z'}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 5); - }); query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'country', from: '', to: 'Z'}); @@ -181,12 +145,6 @@ test('filters with nulls', function () { equal(out.total, 5); }); - query = new recline.Model.Query(); - query.addFilter({type: 'range', field: 'x', start: '', stop: ''}); - data.query(query.toJSON()).then(function(out) { - equal(out.total, 6); - }); - query = new recline.Model.Query(); query.addFilter({type: 'range', field: 'x', from: '', to: ''}); data.query(query.toJSON()).then(function(out) { @@ -350,14 +308,14 @@ test('filters', function () { }); dataset = makeBackendDataset(); - dataset.queryState.addFilter({type: 'range', field: 'date', start: '2011-01-01', stop: '2011-05-01'}); + dataset.queryState.addFilter({type: 'range', field: 'date', from: '2011-01-01', to: '2011-05-01'}); dataset.query().then(function() { equal(dataset.records.length, 3); deepEqual(dataset.records.pluck('date'), ['2011-01-01','2011-02-03','2011-04-05']); }); dataset = makeBackendDataset(); - dataset.queryState.addFilter({type: 'range', field: 'z', start: '0', stop: '10'}); + dataset.queryState.addFilter({type: 'range', field: 'z', from: '0', to: '10'}); dataset.query().then(function() { equal(dataset.records.length, 3); deepEqual(dataset.records.pluck('z'), [3,6,9]); diff --git a/test/widget.filtereditor.test.js b/test/widget.filtereditor.test.js index 656185c2..5faa0189 100644 --- a/test/widget.filtereditor.test.js +++ b/test/widget.filtereditor.test.js @@ -41,7 +41,7 @@ test('basics', function () { $editForm.find('.filter-range input').last().val('4'); $editForm.submit(); equal(dataset.queryState.attributes.filters[0].term, 'UK'); - equal(dataset.queryState.attributes.filters[1].start, 2); + equal(dataset.queryState.attributes.filters[1].from, 2); equal(dataset.records.length, 2); // now remove filter From f96099513e19858d14642827a69362b24f91a2b1 Mon Sep 17 00:00:00 2001 From: mjuniper Date: Tue, 10 Dec 2013 15:24:21 -0700 Subject: [PATCH 68/74] Don't override backbone.sync globally; only inside recline.Dataset. --- src/model.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/model.js b/src/model.js index a70ba1ac..914b72b6 100644 --- a/src/model.js +++ b/src/model.js @@ -52,6 +52,10 @@ my.Dataset = Backbone.Model.extend({ } }, + sync: function(method, model, options) { + return this.backend.sync(method, model, options); + }, + // ### fetch // // Retrieve dataset and (some) records from the backend. @@ -635,9 +639,9 @@ my.ObjectState = Backbone.Model.extend({ // ## Backbone.sync // // Override Backbone.sync to hand off to sync function in relevant backend -Backbone.sync = function(method, model, options) { - return model.backend.sync(method, model, options); -}; +// Backbone.sync = function(method, model, options) { +// return model.backend.sync(method, model, options); +// }; }(this.recline.Model)); From a35edd8815b4e274a7a1be181c540fead3fe9379 Mon Sep 17 00:00:00 2001 From: Suz Date: Wed, 18 Dec 2013 17:59:39 -0800 Subject: [PATCH 69/74] added test data for Edmonton and Rio de Janerio in DMS format --- test/view.map.test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/view.map.test.js b/test/view.map.test.js index e3af2581..57688f6d 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -113,7 +113,10 @@ test('_getGeometryFromRecord non-GeoJSON', function () { ["53.3,47.32", [47.32, 53.3]], ["53.3, 47.32", [47.32, 53.3]], ["(53.3,47.32)", [47.32, 53.3]], - [[53.3,47.32], [53.3, 47.32]] + [[53.3,47.32], [53.3, 47.32]], + ["53.3 N, 113.5 W", [53.3, -113.5]], + ["53° 18' N, 113° 30' W", [53.3, -113.5]] + ["22°54′30″S 43°11′47″W", [-22.983, -43.3139]] ]; var view = new recline.View.Map({ model: new recline.Model.Dataset({ @@ -219,7 +222,7 @@ test('geoJsonLayerOptions', function () { model: dataset }); $('.fixtures').append(view.el); - view.geoJsonLayerOptions.point + view.geoJsonLayerOptions.point view.geoJsonLayerOptions.pointToLayer = function(feature, latlng) { var marker = new L.CircleMarker(latlng, { radius: 8 } ); marker.bindPopup(feature.properties.popupContent); From 27627c45d942691b2906d1e6f17ae16463c74213 Mon Sep 17 00:00:00 2001 From: Suz Date: Fri, 20 Dec 2013 17:01:18 -0800 Subject: [PATCH 70/74] Corrected syntax of new mapping test cases --- test/view.map.test.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/view.map.test.js b/test/view.map.test.js index 57688f6d..d867410f 100644 --- a/test/view.map.test.js +++ b/test/view.map.test.js @@ -110,13 +110,14 @@ test('GeoJSON geom field', function () { test('_getGeometryFromRecord non-GeoJSON', function () { var test = [ [{ lon: 47, lat: 53}, [47,53]], + [{ lon: -47, lat: 53}, [-47,53]], ["53.3,47.32", [47.32, 53.3]], ["53.3, 47.32", [47.32, 53.3]], ["(53.3,47.32)", [47.32, 53.3]], [[53.3,47.32], [53.3, 47.32]], - ["53.3 N, 113.5 W", [53.3, -113.5]], - ["53° 18' N, 113° 30' W", [53.3, -113.5]] - ["22°54′30″S 43°11′47″W", [-22.983, -43.3139]] + ["53.3 N, 113.5 W", [-113.5, 53.3]], + ["53° 18' N, 113° 30' W", [-113.5, 53.3 ]], + ["22°45′90″S, 43°15′45″W", [-43.2625, -22.775]] ]; var view = new recline.View.Map({ model: new recline.Model.Dataset({ From b744c88eb04700d2f29014b0093a1c9fe108c291 Mon Sep 17 00:00:00 2001 From: Suz Date: Fri, 20 Dec 2013 17:27:10 -0800 Subject: [PATCH 71/74] fix issue 380 -- added function to parse DMS formatted map data (36d 56m 29s S) --- src/view.map.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/view.map.js b/src/view.map.js index b4014123..a7af1fcf 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -10,7 +10,7 @@ this.recline.View = this.recline.View || {}; // This view allows to plot gereferenced records on a map. The location // information can be provided in 2 ways: // -// 1. Via a single field. This field must be either a geo_point or +// 1. Via a single field. This field must be either a geo_point or // [GeoJSON](http://geojson.org) object // 2. Via two fields with latitude and longitude coordinates. // @@ -326,6 +326,32 @@ my.Map = Backbone.View.extend({ }, + // Private: convert DMS coordinates to decimal + // + // north and east are positive, south and west are negative + // + _parseCoordinateString: function(coord){ + if (typeof(coord) != 'string') { + return(parseFloat(coord)); + } + var dms = coord.split(/[^\.\d\w]+/); + console.log(dms); + var deg = 0; var m = 0; + var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec + var i; + for (i = 0; i < dms.length; ++i) { + if (isNaN(parseFloat(dms[i]))) { + continue; + } + deg += parseFloat(dms[i]) / toDeg[m]; + m += 1; + } + if (coord.match(/[SW]/)) { + deg = -1*deg; + } + return(deg); + }, + // Private: Return a GeoJSON geomtry extracted from the record fields // _getGeometryFromRecord: function(doc){ @@ -337,12 +363,12 @@ my.Map = Backbone.View.extend({ value = $.parseJSON(value); } catch(e) {} } - if (typeof(value) === 'string') { value = value.replace('(', '').replace(')', ''); var parts = value.split(','); - var lat = parseFloat(parts[0]); - var lon = parseFloat(parts[1]); + var lat = this._parseCoordinateString(parts[0]); + var lon = this._parseCoordinateString(parts[1]); + if (!isNaN(lon) && !isNaN(parseFloat(lat))) { return { "type": "Point", @@ -370,6 +396,9 @@ my.Map = Backbone.View.extend({ // We'll create a GeoJSON like point object from the two lat/lon fields var lon = doc.get(this.state.get('lonField')); var lat = doc.get(this.state.get('latField')); + lon = this._parseCoordinateString(lon); + lat = this._parseCoordinateString(lat); + if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) { return { type: 'Point', From 14f3545ae6e9e9f2e75ad7674143ad92ae59caa6 Mon Sep 17 00:00:00 2001 From: S Kiihne Date: Sun, 29 Dec 2013 12:26:09 -0800 Subject: [PATCH 72/74] removed 'console.log' statement used for debugging --- src/view.map.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/view.map.js b/src/view.map.js index a7af1fcf..a90270ce 100644 --- a/src/view.map.js +++ b/src/view.map.js @@ -335,7 +335,6 @@ my.Map = Backbone.View.extend({ return(parseFloat(coord)); } var dms = coord.split(/[^\.\d\w]+/); - console.log(dms); var deg = 0; var m = 0; var toDeg = [1, 60, 3600]; // conversion factors for Deg, min, sec var i; From 964a59310696f7c107e0b6cc21266ea7b41c82ac Mon Sep 17 00:00:00 2001 From: kielni Date: Mon, 6 Jan 2014 14:56:31 -0800 Subject: [PATCH 73/74] fix variable hiding when finding filter to replace --- src/model.js | 4 ++-- test/model.test.js | 42 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/model.js b/src/model.js index 914b72b6..ee6b84b3 100644 --- a/src/model.js +++ b/src/model.js @@ -522,8 +522,8 @@ my.Query = Backbone.Model.extend({ // delete filter on the same field, then add var filters = this.get('filters'); var idx = -1; - _.each(this.get('filters'), function(filter, key, list) { - if (filter.field == filter.field) { + _.each(this.get('filters'), function(f, key, list) { + if (filter.field == f.field) { idx = key; } }); diff --git a/test/model.test.js b/test/model.test.js index e052a391..3103dccd 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -400,22 +400,54 @@ test('Query.addFilter', function () { test('Query.replaceFilter', function () { var query = new recline.Model.Query(); - query.addFilter({type: 'term', field: 'xyz'}); + query.addFilter({type: 'term', field: 'xyz', term: 'one'}); var exp = { field: 'xyz', type: 'term', - term: '' + term: 'one' }; deepEqual(query.get('filters')[0], exp); - query.replaceFilter({type: 'term', field: 'abc'}); + query.replaceFilter({type: 'term', field: 'xyz', term: 'two'}); exp = { - field: 'abc', + field: 'xyz', type: 'term', - term: '' + term: 'two' }; deepEqual(query.get('filters')[0], exp); }); +test('Query.replaceFilter first filter', function () { + // replaceFilter changes filter order + var query = new recline.Model.Query(); + query.addFilter({type: 'term', field: 'abc', term: 'one'}); + query.addFilter({type: 'term', field: 'xyz', term: 'two'}); + var exp0 = { + field: 'abc', + type: 'term', + term: 'one' + }; + deepEqual(query.get('filters')[0], exp0); + var exp1 = { + field: 'xyz', + type: 'term', + term: 'two' + }; + deepEqual(query.get('filters')[1], exp1); + + var numFilters = query.get('filters').length; + query.replaceFilter({type: 'term', field: 'abc', term: 'three'}); + equal(query.get('filters').length, numFilters); + exp0 = { + field: 'abc', + type: 'term', + term: 'three' + }; + // deletes original filter and adds replacement to end + deepEqual(query.get('filters')[1], exp0); + deepEqual(query.get('filters')[0], exp1); + +}); + })(this.jQuery); From 45fa438803d7af0e6641a7fce811ca7cf7ad8543 Mon Sep 17 00:00:00 2001 From: Rufus Pollock Date: Sun, 12 Jan 2014 22:44:20 +0000 Subject: [PATCH 74/74] [#384,slickgrid][s]: slickgrid sets editor type automatically from field type. * multiview demo now has editing turned on * [license][xs]: update to 2011-2014. --- LICENSE.txt | 2 +- demos/multiview/app.js | 14 +++++++++++++- src/view.slickgrid.js | 24 +++++++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index ac88e62c..a0993c64 100755 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2011 Max Ogden & Contributors +Copyright (c) 2011-2014 Max Ogden, Rufus Pollock and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/demos/multiview/app.js b/demos/multiview/app.js index 06e3dc61..2aec5896 100755 --- a/demos/multiview/app.js +++ b/demos/multiview/app.js @@ -73,7 +73,18 @@ var createExplorer = function(dataset, state) { id: 'grid', label: 'Grid', view: new recline.View.SlickGrid({ - model: dataset + model: dataset, + state: { + gridOptions: { + editable: true, + enabledAddRow: true, + enableCellNavigation: true + }, + columnsEditor: [ + { column: 'date', editor: Slick.Editors.Date }, + { column: 'title', editor: Slick.Editors.Text } + ] + } }) }, { @@ -81,6 +92,7 @@ var createExplorer = function(dataset, state) { label: 'Graph', view: new recline.View.Graph({ model: dataset + }) }, { diff --git a/src/view.slickgrid.js b/src/view.slickgrid.js index 55477852..f7512f8d 100644 --- a/src/view.slickgrid.js +++ b/src/view.slickgrid.js @@ -23,7 +23,11 @@ this.recline.View = this.recline.View || {}; // model: dataset, // el: $el, // state: { -// gridOptions: {editable: true}, +// gridOptions: { +// editable: true, +// enableAddRows: true +// ... +// }, // columnsEditor: [ // {column: 'date', editor: Slick.Editors.Date }, // {column: 'title', editor: Slick.Editors.Text} @@ -113,6 +117,24 @@ my.SlickGrid = Backbone.View.extend({ var editInfo = _.find(self.state.get('columnsEditor'),function(c){return c.column === field.id;}); if (editInfo){ column.editor = editInfo.editor; + } else { + // guess editor type + var typeToEditorMap = { + 'string': Slick.Editors.LongText, + 'integer': Slick.Editors.IntegerEditor, + 'number': Slick.Editors.Text, + // TODO: need a way to ensure we format date in the right way + // Plus what if dates are in distant past or future ... (?) + // 'date': Slick.Editors.DateEditor, + 'date': Slick.Editors.Text, + 'boolean': Slick.Editors.YesNoSelectEditor + // TODO: (?) percent ... + }; + if (field.type in typeToEditorMap) { + column.editor = typeToEditorMap[field.type] + } else { + column.editor = Slick.Editors.LongText; + } } columns.push(column); });