diff --git a/app/built.html b/app/built.html index 334c47f8..6fb0ed16 100644 --- a/app/built.html +++ b/app/built.html @@ -32,7 +32,6 @@ - diff --git a/app/index.html b/app/index.html index 28f148c3..42666af3 100644 --- a/app/index.html +++ b/app/index.html @@ -32,7 +32,6 @@ - @@ -124,6 +123,50 @@ +
+
+
+
+

Welcome to the Recline Data Explorer

+

The Data Explorer is an application for exploring + and working with data built in pure javascript and html. In basic operation it's much like a spreadsheet - though it's + feature set is a little different. In particular, the Data + Explorer provides: +

    +
  • Data grid / spreadsheet
  • +
  • Data editing including programmatic data transformation in javascript
  • +
  • Visualizations includes graphs and maps
  • +
  • Import and export from a variety of sources including online sources such as online Excel and CSV files, Google docs and + the DataHub and offline sources like CSV files on your local machine.
  • +
  • Use online or offline - because the app is built in pure javascript and html you can use it anywhere there's a modern web browser. Using offline is as easy and downloading this web page to your local machine.
  • +
+
+
+

View the demo

+

Take a look at a local demo dataset.

+

View the demo dataset »

+
+
+

Read the tutorial

+

Take a look at the tutorial for using the data explorer:

+ Read the tutorial » +
+
+

Import some data

+

Starting working with some data straight away. You can import some data using the menu at the top right of this page.

+
+
+
+
+
+ +
+
+
+
+ + + - -
-
-
-
-
diff --git a/app/js/app.js b/app/js/app.js index e6c87123..675914c8 100755 --- a/app/js/app.js +++ b/app/js/app.js @@ -2,7 +2,6 @@ jQuery(function($) { var app = new ExplorerApp({ el: $('.recline-app') }) - Backbone.history.start(); }); var ExplorerApp = Backbone.View.extend({ @@ -15,8 +14,14 @@ var ExplorerApp = Backbone.View.extend({ this.el = $(this.el); this.explorer = null; this.explorerDiv = $('.data-explorer-here'); + _.bindAll(this, 'viewExplorer', 'viewHome'); - var state = recline.View.parseQueryString(window.location.search); + this.router = new Backbone.Router(); + this.router.route('', 'home', this.viewHome); + this.router.route(/explorer/, 'explorer', this.viewExplorer); + Backbone.history.start(); + + var state = recline.Util.parseQueryString(window.location.search); if (state) { _.each(state, function(value, key) { try { @@ -30,18 +35,34 @@ var ExplorerApp = Backbone.View.extend({ } } var dataset = null; - if ( - (state.dataset || state.url) && - // special cases for demo / memory dataset - !(state.url === 'demo' || state.backend === 'memory')) - { - dataset = recline.Model.Dataset.restore(state); - } else { + // special cases for demo / memory dataset + if (state.url === 'demo' || state.backend === 'memory') { dataset = localDataset(); } - this.createExplorer(dataset, state); + else if (state.dataset || state.url) { + dataset = recline.Model.Dataset.restore(state); + } + if (dataset) { + this.createExplorer(dataset, state); + } }, + viewHome: function() { + this.switchView('home'); + }, + + viewExplorer: function() { + this.router.navigate('explorer'); + this.switchView('explorer'); + }, + + switchView: function(path) { + $('.backbone-page').hide(); + var cssClass = path.replace('/', '-'); + $('.page-' + cssClass).show(); + }, + + // make Explorer creation / initialization in a function so we can call it // again and again createExplorer: function(dataset, state) { @@ -63,12 +84,7 @@ var ExplorerApp = Backbone.View.extend({ this._setupPermaLink(this.dataExplorer); this._setupEmbed(this.dataExplorer); - // HACK (a bit). Issue is that Backbone will not trigger the route - // if you are already at that location so we have to make sure we genuinely switch - if (reload) { - // this.dataExplorer.router.navigate('graph'); - // this.dataExplorer.router.navigate('', true); - } + this.viewExplorer(); }, _setupPermaLink: function(explorer) { @@ -96,7 +112,7 @@ var ExplorerApp = Backbone.View.extend({ }, makePermaLink: function(state) { - var qs = recline.View.composeQueryString(state.toJSON()); + var qs = recline.Util.composeQueryString(state.toJSON()); return window.location.origin + window.location.pathname + qs; }, diff --git a/css/grid.css b/css/grid.css index aeb9984e..88f0b134 100644 --- a/css/grid.css +++ b/css/grid.css @@ -210,10 +210,6 @@ div.data-table-cell-content-numeric > a.data-table-cell-edit { * Transform Dialog *********************************************************/ -#expression-preview-tabs .ui-tabs-nav li a { - padding: 0.15em 1em; -} - textarea.expression-preview-code { font-family: monospace; height: 5em; diff --git a/src/util.js b/src/util.js index cd0086be..55e9390a 100644 --- a/src/util.js +++ b/src/util.js @@ -1,153 +1,79 @@ /*jshint multistr:true */ -var util = function() { - var templates = { - transformActions: '
  • Global transform...
  • ', - cellEditor: ' \ - \ - ', - editPreview: ' \ -
    \ - \ - \ - \ - \ - \ - \ - \ - \ - {{#rows}} \ - \ - \ - \ - \ - {{/rows}} \ - \ -
    \ - before \ - \ - after \ -
    \ - {{before}} \ - \ - {{after}} \ -
    \ -
    \ - ' - }; +this.recline = this.recline || {}; +this.recline.Util = this.recline.Util || {}; - $.fn.serializeObject = function() { - var o = {}; - var a = this.serializeArray(); - $.each(a, function() { - if (o[this.name]) { - if (!o[this.name].push) { - o[this.name] = [o[this.name]]; - } - o[this.name].push(this.value || ''); - } else { - o[this.name] = this.value || ''; - } - }); - return o; - }; +(function(my) { +// ## Miscellaneous Utilities - function registerEmitter() { - var Emitter = function(obj) { - this.emit = function(obj, channel) { - if (!channel) channel = 'data'; - this.trigger(channel, obj); - }; +var urlPathRegex = /^([^?]+)(\?.*)?/; + +// Parse the Hash section of a URL into path and query string +my.parseHashUrl = function(hashUrl) { + var parsed = urlPathRegex.exec(hashUrl); + if (parsed === null) { + return {}; + } else { + return { + path: parsed[1], + query: parsed[2] || '' }; - MicroEvent.mixin(Emitter); - return new Emitter(); - } - - function listenFor(keys) { - var shortcuts = { // from jquery.hotkeys.js - 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" - }; - window.addEventListener("keyup", function(e) { - var pressed = shortcuts[e.keyCode]; - if(_.include(keys, pressed)) app.emitter.emit("keyup", pressed); - }, false); - } - - function observeExit(elem, callback) { - var cancelButton = elem.find('.cancelButton'); - // TODO: remove (commented out as part of Backbon-i-fication - // app.emitter.on('esc', function() { - // cancelButton.click(); - // app.emitter.clear('esc'); - // }); - cancelButton.click(callback); - } - - function show( thing ) { - $('.' + thing ).show(); - $('.' + thing + '-overlay').show(); } +}; - function hide( thing ) { - $('.' + thing ).hide(); - $('.' + thing + '-overlay').hide(); - // TODO: remove or replace (commented out as part of Backbon-i-fication - // if (thing === "dialog") app.emitter.clear('esc'); // todo more elegant solution - } - - function position( thing, elem, offset ) { - var position = $(elem.target).position(); - if (offset) { - if (offset.top) position.top += offset.top; - if (offset.left) position.left += offset.left; - } - $('.' + thing + '-overlay').show().click(function(e) { - $(e.target).hide(); - $('.' + thing).hide(); - }); - $('.' + thing).show().css({top: position.top + $(elem.target).height(), left: position.left}); +// Parse a URL query string (?xyz=abc...) into a dictionary. +my.parseQueryString = function(q) { + if (!q) { + return {}; } + var urlParams = {}, + e, d = function (s) { + return unescape(s.replace(/\+/g, " ")); + }, + r = /([^&=]+)=?([^&]*)/g; - function render( template, target, options ) { - if ( !options ) options = {data: {}}; - if ( !options.data ) options = {data: options}; - var html = $.mustache( templates[template], options.data ); - var targetDom = null; - if (target instanceof jQuery) { - targetDom = target; - } else { - targetDom = $( "." + target + ":first" ); - } - if( options.append ) { - targetDom.append( html ); - } else { - targetDom.html( html ); - } - // TODO: remove (commented out as part of Backbon-i-fication - // if (template in app.after) app.after[template](); + if (q && q.length && q[0] === '?') { + q = q.slice(1); } + while (e = r.exec(q)) { + // TODO: have values be array as query string allow repetition of keys + urlParams[d(e[1])] = d(e[2]); + } + return urlParams; +}; + +// Parse the query string out of the URL hash +my.parseHashQueryString = function() { + q = my.parseHashUrl(window.location.hash).query; + return my.parseQueryString(q); +}; + +// Compse a Query String +my.composeQueryString = function(queryParams) { + var queryString = '?'; + var items = []; + $.each(queryParams, function(key, value) { + if (typeof(value) === 'object') { + value = JSON.stringify(value); + } + items.push(key + '=' + value); + }); + queryString += items.join('&'); + return queryString; +}; + +my.getNewHashForQueryString = function(queryParams) { + var queryPart = my.composeQueryString(queryParams); + if (window.location.hash) { + // slice(1) to remove # at start + return window.location.hash.split('?')[0].slice(1) + queryPart; + } else { + return queryPart; + } +}; + +my.setHashQueryString = function(queryParams) { + window.location.hash = my.getNewHashForQueryString(queryParams); +}; +})(this.recline.Util); - return { - registerEmitter: registerEmitter, - listenFor: listenFor, - show: show, - hide: hide, - position: position, - render: render, - observeExit: observeExit - }; -}(); diff --git a/src/view-grid.js b/src/view-grid.js index dc616713..30e81a33 100644 --- a/src/view-grid.js +++ b/src/view-grid.js @@ -35,18 +35,6 @@ my.Grid = Backbone.View.extend({ 'click .data-table-menu li a': 'onMenuClick' }, - // TODO: delete or re-enable (currently this code is not used from anywhere except deprecated or disabled methods (see above)). - // showDialog: function(template, data) { - // if (!data) data = {}; - // util.show('dialog'); - // util.render(template, 'dialog-content', data); - // util.observeExit($('.dialog-content'), function() { - // util.hide('dialog'); - // }) - // $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - // }, - - // ====================================================== // Column and row menus @@ -81,12 +69,12 @@ my.Grid = Backbone.View.extend({ filter: function() { self.model.queryState.addTermFilter(self.tempState.currentColumn, ''); }, - transform: function() { self.showTransformDialog('transform'); }, sortAsc: function() { self.setColumnSort('asc'); }, sortDesc: function() { self.setColumnSort('desc'); }, hideColumn: function() { self.hideColumn(); }, showColumn: function() { self.showColumn(e); }, deleteRow: function() { + var self = this; var doc = _.find(self.model.currentDocuments.models, function(doc) { // important this is == as the currentRow will be string (as comes // from DOM) while id may be int @@ -94,9 +82,9 @@ my.Grid = Backbone.View.extend({ }); doc.destroy().then(function() { self.model.currentDocuments.remove(doc); - my.notify("Row deleted successfully"); + self.trigger('recline:flash', {message: "Row deleted successfully"}); }).fail(function(err) { - my.notify("Errorz! " + err); + self.trigger('recline:flash', {message: "Errorz! " + err}); }); } }; @@ -104,33 +92,18 @@ my.Grid = Backbone.View.extend({ }, showTransformColumnDialog: function() { - var $el = $('.dialog-content'); - util.show('dialog'); + var self = this; var view = new my.ColumnTransform({ model: this.model }); + // pass the flash message up the chain + view.bind('recline:flash', function(flash) { + self.trigger('recline:flash', flash); + }); view.state = this.tempState; view.render(); - $el.empty(); - $el.append(view.el); - util.observeExit($el, function() { - util.hide('dialog'); - }); - $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); - }, - - showTransformDialog: function() { - var $el = $('.dialog-content'); - util.show('dialog'); - var view = new recline.View.DataTransform({ - }); - view.render(); - $el.empty(); - $el.append(view.el); - util.observeExit($el, function() { - util.hide('dialog'); - }); - $('.dialog').draggable({ handle: '.dialog-header', cursor: 'move' }); + this.el.append(view.el); + view.el.modal(); }, setColumnSort: function(order) { @@ -293,6 +266,19 @@ my.GridRow = Backbone.View.extend({ // =================== // Cell Editor methods + + cellEditorTemplate: ' \ + \ + ', + onEditClick: function(e) { var editing = this.el.find('.data-table-cell-editor-editor'); if (editing.length > 0) { @@ -301,10 +287,12 @@ my.GridRow = Backbone.View.extend({ $(e.target).addClass("hidden"); var cell = $(e.target).siblings('.data-table-cell-value'); cell.data("previousContents", cell.text()); - util.render('cellEditor', cell, {value: cell.text()}); + var templated = $.mustache(this.cellEditorTemplate, {value: cell.text()}); + cell.html(templated); }, onEditorOK: function(e) { + var self = this; var cell = $(e.target); var rowId = cell.parents('tr').attr('data-id'); var field = cell.parents('td').attr('data-field'); @@ -312,12 +300,13 @@ my.GridRow = Backbone.View.extend({ var newData = {}; newData[field] = newValue; this.model.set(newData); - my.notify("Updating row...", {loader: true}); + this.trigger('recline:flash', {message: "Updating row...", loader: true}); this.model.save().then(function(response) { - my.notify("Row updated successfully", {category: 'success'}); + this.trigger('recline:flash', {message: "Row updated successfully", category: 'success'}); }) .fail(function() { - my.notify('Error saving row', { + this.trigger('recline:flash', { + message: 'Error saving row', category: 'error', persist: true }); diff --git a/src/view-map.js b/src/view-map.js index 97d49499..5b2f3433 100644 --- a/src/view-map.js +++ b/src/view-map.js @@ -281,7 +281,6 @@ my.Map = Backbone.View.extend({ // Each feature will have a popup associated with all the document fields. // _add: function(docs){ - var self = this; if (!(docs instanceof Array)) docs = [docs]; @@ -316,13 +315,13 @@ my.Map = Backbone.View.extend({ var msg = 'Wrong geometry value'; if (except.message) msg += ' (' + except.message + ')'; if (wrongSoFar <= 10) { - my.notify(msg,{category:'error'}); + self.trigger('recline:flash', {message: msg, category:'error'}); } } } else { wrongSoFar += 1 if (wrongSoFar <= 10) { - my.notify('Wrong geometry value',{category:'error'}); + self.trigger('recline:flash', {message: 'Wrong geometry value', category:'error'}); } } return true; diff --git a/src/view-transform-dialog.js b/src/view-transform-dialog.js index 12f83872..6c69766b 100644 --- a/src/view-transform-dialog.js +++ b/src/view-transform-dialog.js @@ -6,82 +6,17 @@ this.recline.View = this.recline.View || {}; // Views module following classic module pattern (function($, my) { -// View (Dialog) for doing data transformations on whole dataset. -my.DataTransform = Backbone.View.extend({ - className: 'transform-view', - template: ' \ -
    \ - Recursive transform on all rows \ -
    \ -
    \ -
    \ -

    Traverse and transform objects by visiting every node on a recursive walk using js-traverse.

    \ - \ - \ - \ - \ - \ - \ -
    \ -
    \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ -
    \ - Expression \ -
    \ -
    \ - \ -
    \ -
    \ - No syntax error. \ -
    \ -
    \ - Preview \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ -
    \ - \ - ', - - initialize: function() { - this.el = $(this.el); - }, - - render: function() { - this.el.html(this.template); - } -}); - - +// ## ColumnTransform +// // View (Dialog) for doing data transformations (on columns of data). my.ColumnTransform = Backbone.View.extend({ - className: 'transform-column-view', + className: 'transform-column-view modal fade in', template: ' \ -
    \ - Functional transform on column {{name}} \ + \ -
    \ + \ - \
    \ - \ - \
    \ ', events: { @@ -215,6 +209,7 @@ my.DataExplorer = Backbone.View.extend({ // these must be called after pageViews are created this.render(); this._bindStateChanges(); + this._bindFlashNotifications(); // now do updates based on state (need to come after render) if (this.state.get('readOnly')) { this.setReadOnly(); @@ -225,24 +220,16 @@ my.DataExplorer = Backbone.View.extend({ this.updateNav(this.pageViews[0].id); } - this.router = new Backbone.Router(); - this.setupRouting(); - this.model.bind('query:start', function() { - my.notify('Loading data', {loader: true}); + self.notify({message: 'Loading data', loader: true}); }); this.model.bind('query:done', function() { - my.clearNotifications(); + self.clearNotifications(); self.el.find('.doc-count').text(self.model.docCount || 'Unknown'); - my.notify('Data loaded', {category: 'success'}); - // update navigation - var qs = my.parseHashQueryString(); - qs.reclineQuery = JSON.stringify(self.model.queryState.toJSON()); - var out = my.getNewHashForQueryString(qs); - // self.router.navigate(out); + self.notify({message: 'Data loaded', category: 'success'}); }); this.model.bind('query:fail', function(error) { - my.clearNotifications(); + self.clearNotifications(); var msg = ''; if (typeof(error) == 'string') { msg = error; @@ -256,7 +243,7 @@ my.DataExplorer = Backbone.View.extend({ } else { msg = 'There was an error querying the backend'; } - my.notify(msg, {category: 'error', persist: true}); + self.notify({message: msg, category: 'error', persist: true}); }); // retrieve basic data like fields etc @@ -266,7 +253,7 @@ my.DataExplorer = Backbone.View.extend({ self.model.query(self.state.get('query')); }) .fail(function(error) { - my.notify(error.message, {category: 'error', persist: true}); + self.notify({message: error.message, category: 'error', persist: true}); }); }, @@ -299,21 +286,6 @@ my.DataExplorer = Backbone.View.extend({ this.el.find('.header').append(facetViewer.el); }, - setupRouting: function() { - var self = this; - // Default route -// this.router.route(/^(\?.*)?$/, this.pageViews[0].id, function(queryString) { -// self.updateNav(self.pageViews[0].id, queryString); -// }); -// $.each(this.pageViews, function(idx, view) { -// self.router.route(/^([^?]+)(\?.*)?/, 'view', function(viewId, queryString) { -// self.updateNav(viewId, queryString); -// }); -// }); - this.router.route(/.*/, 'view', function() { - }); - }, - updateNav: function(pageName) { this.el.find('.navigation li').removeClass('active'); this.el.find('.navigation li a').removeClass('disabled'); @@ -357,7 +329,7 @@ my.DataExplorer = Backbone.View.extend({ _setupState: function(initialState) { var self = this; // get data from the query string / hash url plus some defaults - var qs = my.parseHashQueryString(); + var qs = recline.Util.parseHashQueryString(); var query = qs.reclineQuery; query = query ? JSON.parse(query) : self.model.queryState.toJSON(); // backwards compatability (now named view-graph but was named graph) @@ -397,6 +369,57 @@ my.DataExplorer = Backbone.View.extend({ }); } }); + }, + + _bindFlashNotifications: function() { + var self = this; + _.each(this.pageViews, function(pageView) { + pageView.view.bind('recline:flash', function(flash) { + self.notify(flash); + }); + }); + }, + + // ### notify + // + // Create a notification (a div.alert in div.alert-messsages) using provided + // flash object. Flash attributes (all are optional): + // + // * message: message to show. + // * category: warning (default), success, error + // * persist: if true alert is persistent, o/w hidden after 3s (default = false) + // * loader: if true show loading spinner + notify: function(flash) { + var tmplData = _.extend({ + message: '', + category: 'warning' + }, + flash + ); + var _template = ' \ +
    × \ + {{message}} \ + {{#loader}} \ +   \ + {{/loader}} \ +
    '; + var _templated = $.mustache(_template, tmplData); + _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); + if (!flash.persist) { + setTimeout(function() { + $(_templated).fadeOut(1000, function() { + $(this).remove(); + }); + }, 1000); + } + }, + + // ### clearNotifications + // + // Clear all existing notifications + clearNotifications: function() { + var $notifications = $('.recline-data-explorer .alert-messages .alert'); + $notifications.remove(); } }); @@ -637,118 +660,6 @@ my.FacetViewer = Backbone.View.extend({ } }); -/* ========================================================== */ -// ## Miscellaneous Utilities - -var urlPathRegex = /^([^?]+)(\?.*)?/; - -// Parse the Hash section of a URL into path and query string -my.parseHashUrl = function(hashUrl) { - var parsed = urlPathRegex.exec(hashUrl); - if (parsed === null) { - return {}; - } else { - return { - path: parsed[1], - query: parsed[2] || '' - }; - } -}; - -// Parse a URL query string (?xyz=abc...) into a dictionary. -my.parseQueryString = function(q) { - if (!q) { - return {}; - } - var urlParams = {}, - e, d = function (s) { - return unescape(s.replace(/\+/g, " ")); - }, - r = /([^&=]+)=?([^&]*)/g; - - if (q && q.length && q[0] === '?') { - q = q.slice(1); - } - while (e = r.exec(q)) { - // TODO: have values be array as query string allow repetition of keys - urlParams[d(e[1])] = d(e[2]); - } - return urlParams; -}; - -// Parse the query string out of the URL hash -my.parseHashQueryString = function() { - q = my.parseHashUrl(window.location.hash).query; - return my.parseQueryString(q); -}; - -// Compse a Query String -my.composeQueryString = function(queryParams) { - var queryString = '?'; - var items = []; - $.each(queryParams, function(key, value) { - if (typeof(value) === 'object') { - value = JSON.stringify(value); - } - items.push(key + '=' + value); - }); - queryString += items.join('&'); - return queryString; -}; - -my.getNewHashForQueryString = function(queryParams) { - var queryPart = my.composeQueryString(queryParams); - if (window.location.hash) { - // slice(1) to remove # at start - return window.location.hash.split('?')[0].slice(1) + queryPart; - } else { - return queryPart; - } -}; - -my.setHashQueryString = function(queryParams) { - window.location.hash = my.getNewHashForQueryString(queryParams); -}; - -// ## notify -// -// Create a notification (a div.alert in div.alert-messsages) using provide messages and options. Options are: -// -// * category: warning (default), success, error -// * persist: if true alert is persistent, o/w hidden after 3s (default = false) -// * loader: if true show loading spinner -my.notify = function(message, options) { - if (!options) options = {}; - var tmplData = _.extend({ - msg: message, - category: 'warning' - }, - options); - var _template = ' \ -
    × \ - {{msg}} \ - {{#loader}} \ -   \ - {{/loader}} \ -
    '; - var _templated = $.mustache(_template, tmplData); - _templated = $(_templated).appendTo($('.recline-data-explorer .alert-messages')); - if (!options.persist) { - setTimeout(function() { - $(_templated).fadeOut(1000, function() { - $(this).remove(); - }); - }, 1000); - } -}; - -// ## clearNotifications -// -// Clear all existing notifications -my.clearNotifications = function() { - var $notifications = $('.recline-data-explorer .alert-messages .alert'); - $notifications.remove(); -}; })(jQuery, recline.View); diff --git a/test/index.html b/test/index.html index bb721fdd..8e25198c 100644 --- a/test/index.html +++ b/test/index.html @@ -10,7 +10,6 @@ - @@ -22,6 +21,7 @@ + diff --git a/test/util.test.js b/test/util.test.js index e6711c0d..c4d7f930 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -2,10 +2,10 @@ module("Util"); test('parseHashUrl', function () { - var out = recline.View.parseHashUrl('graph?x=y'); + var out = recline.Util.parseHashUrl('graph?x=y'); equal(out.path, 'graph'); equal(out.query, '?x=y'); - var out = recline.View.parseHashUrl('graph'); + var out = recline.Util.parseHashUrl('graph'); equal(out.path, 'graph'); equal(out.query, ''); }); @@ -15,7 +15,7 @@ test('composeQueryString', function () { x: 'y', a: 'b' }; - var out = recline.View.composeQueryString(params); + var out = recline.Util.composeQueryString(params); equal(out, '?x=y&a=b'); }); diff --git a/test/view-graph.test.js b/test/view-graph.test.js index 3f65611a..3b015f12 100644 --- a/test/view-graph.test.js +++ b/test/view-graph.test.js @@ -49,5 +49,5 @@ test('dates in graph view', function () { }); $('.fixtures').append(view.el); - // view.remove(); + view.remove(); }); diff --git a/vendor/jquery-ui-1.8.14.custom.min.js b/vendor/jquery-ui-1.8.14.custom.min.js deleted file mode 100755 index d1949182..00000000 --- a/vendor/jquery-ui-1.8.14.custom.min.js +++ /dev/null @@ -1,100 +0,0 @@ -/*! - * jQuery UI 1.8.14 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.14", -keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus(); -b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this, -"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection", -function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth, -outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a,"tabindex"),d=isNaN(b); -return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e= -0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a);return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){b(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted= -false;a.target==this._mouseDownEvent.target&&b.data(a.target,this.widgetName+".preventClickEvent",true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); -;/* - * jQuery UI Draggable 1.8.14 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI/Draggables - * - * Depends: - * jquery.ui.core.js - * jquery.ui.mouse.js - * jquery.ui.widget.js - */ -(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== -"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= -this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;d(b.iframeFix===true?"iframe":b.iframeFix).each(function(){d('
    ').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")});return true},_mouseStart:function(a){var b=this.options;this.helper= -this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}); -this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions();d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);d.ui.ddmanager&&d.ui.ddmanager.dragStart(this,a);return true}, -_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b= -false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if((!this.element[0]||!this.element[0].parentNode)&&this.options.helper=="original")return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element,b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration, -10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},_mouseUp:function(a){this.options.iframeFix===true&&d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)});d.ui.ddmanager&&d.ui.ddmanager.dragStop(this,a);return d.ui.mouse.prototype._mouseUp.call(this,a)},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle|| -!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this==a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone().removeAttr("id"):this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&& -a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]||0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent= -this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"), -10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"), -10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment=="parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[a.containment=="document"?0:d(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,a.containment=="document"?0:d(window).scrollTop()-this.offset.relative.top-this.offset.parent.top, -(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"?0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){a=d(a.containment);var b=a[0];if(b){a.offset();var c=d(b).css("overflow")!= -"hidden";this.containment=[(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0),(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0),(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"), -10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom];this.relative_container=a}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+ -this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&& -!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,h=a.pageY;if(this.originalPosition){var g;if(this.containment){if(this.relative_container){g=this.relative_container.offset();g=[this.containment[0]+g.left,this.containment[1]+g.top,this.containment[2]+g.left,this.containment[3]+g.top]}else g=this.containment;if(a.pageX-this.offset.click.leftg[2])e=g[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>g[3])h=g[3]+this.offset.click.top}if(b.grid){h=b.grid[1]?this.originalPageY+Math.round((h-this.originalPageY)/b.grid[1])*b.grid[1]:this.originalPageY;h=g?!(h-this.offset.click.topg[3])?h:!(h-this.offset.click.topg[2])?e:!(e-this.offset.click.left=0;i--){var j=c.snapElements[i].left,l=j+c.snapElements[i].width,k=c.snapElements[i].top,m=k+c.snapElements[i].height;if(j-e