diff --git a/js/tabletop.js b/js/tabletop.js index 2f2b7a8..e4562f5 100644 --- a/js/tabletop.js +++ b/js/tabletop.js @@ -2,26 +2,39 @@ "use strict"; var inNodeJS = false; - if (typeof process !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { inNodeJS = true; var request = require('request'); } - if (!Array.prototype.indexOf) { - Array.prototype.indexOf = function (obj, fromIndex) { - if (fromIndex === null) { - fromIndex = 0; - } else if (fromIndex < 0) { - fromIndex = Math.max(0, this.length + fromIndex); + var supportsCORS = false; + var inLegacyIE = false; + try { + var testXHR = new XMLHttpRequest(); + if (typeof testXHR.withCredentials !== 'undefined') { + supportsCORS = true; + } else { + if ("XDomainRequest" in window) { + supportsCORS = true; + inLegacyIE = true; } - for (var i = fromIndex, j = this.length; i < j; i++) { - if (this[i] === obj) { - return i; - } - } - return -1; - }; - } + } + } catch (e) { } + + // Create a simple indexOf function for support + // of older browsers. Uses native indexOf if + // available. Code similar to underscores. + // By making a separate function, instead of adding + // to the prototype, we will not break bad for loops + // in older browsers + var indexOfProto = Array.prototype.indexOf; + var ttIndexOf = function(array, item) { + var i = 0, l = array.length; + + if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; /* Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) @@ -47,18 +60,24 @@ this.simpleSheet = !!options.simpleSheet; this.parseNumbers = !!options.parseNumbers; this.wait = !!options.wait; + this.reverse = !!options.reverse; this.postProcess = options.postProcess; this.debug = !!options.debug; this.query = options.query || ''; + this.orderby = options.orderby; this.endpoint = options.endpoint || "https://spreadsheets.google.com"; this.singleton = !!options.singleton; this.simple_url = !!options.simple_url; this.callbackContext = options.callbackContext; if(typeof(options.proxy) !== 'undefined') { - this.endpoint = options.proxy; + // Remove trailing slash, it will break the app + this.endpoint = options.proxy.replace(/\/$/,''); this.simple_url = true; this.singleton = true; + // Let's only use CORS (straight JSON request) when + // fetching straight from Google + supportsCORS = false } this.parameterize = options.parameterize || false; @@ -72,10 +91,16 @@ /* Be friendly about what you accept */ if(/key=/.test(this.key)) { - this.log("You passed a key as a URL! Attempting to parse."); + this.log("You passed an old Google Docs url as the key! Attempting to parse."); this.key = this.key.match("key=(.*?)&")[1]; } + if(/pubhtml/.test(this.key)) { + alert("You passed a new Google Spreadsheets url as the key! This won't work yet, you'll need to change back to old Sheets."); + this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1]; + console.log(this.key); + } + if(!this.key) { this.log("You need to pass Tabletop a key!"); return; @@ -88,7 +113,7 @@ this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; - if (inNodeJS) { + if (inNodeJS || supportsCORS) { this.base_json_path += 'json'; } else { this.base_json_path += 'json-in-script'; @@ -129,9 +154,35 @@ if (inNodeJS) { this.serverSideFetch(path, callback); } else { - this.injectScript(path, callback); + //CORS only works in IE8/9 across the same protocol + //You must have your server on HTTPS to talk to Google, or it'll fall back on injection + var protocol = this.endpoint.split("//").shift() || "http"; + if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { + this.xhrFetch(path, callback); + } else { + this.injectScript(path, callback); + } } }, + + /* + Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. + */ + xhrFetch: function(path, callback) { + //support IE8's separate cross-domain object + var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); + xhr.open("GET", this.endpoint + path); + var self = this; + xhr.onload = function() { + try { + var json = JSON.parse(xhr.responseText); + } catch (e) { + console.error(e); + } + callback.call(self, json); + }; + xhr.send(); + }, /* Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data @@ -207,7 +258,7 @@ if(this.wanted.length === 0) { return true; } else { - return this.wanted.indexOf(sheetName) !== -1; + return (ttIndexOf(this.wanted, sheetName) !== -1); } }, @@ -236,7 +287,7 @@ Add another sheet to the wanted list */ addWanted: function(sheet) { - if(this.wanted.indexOf(sheet) === -1) { + if(ttIndexOf(this.wanted, sheet) === -1) { this.wanted.push(sheet); } }, @@ -260,11 +311,17 @@ if( this.isWanted(data.feed.entry[i].content.$t) ) { var sheet_id = data.feed.entry[i].link[3].href.substr( data.feed.entry[i].link[3].href.length - 3, 3); var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?sq=" + this.query + '&alt=' - if (inNodeJS) { + if (inNodeJS || supportsCORS) { json_path += 'json'; } else { json_path += 'json-in-script'; } + if(this.orderby) { + json_path += "&orderby=column:" + this.orderby.toLowerCase(); + } + if(this.reverse) { + json_path += "&reverse=true"; + } toLoad.push(json_path); } } @@ -304,7 +361,7 @@ postProcess: this.postProcess, tabletop: this } ); this.models[ model.name ] = model; - if(this.model_names.indexOf(model.name) === -1) { + if(ttIndexOf(this.model_names, model.name) === -1) { this.model_names.push(model.name); } this.sheetsToLoad--; @@ -411,4 +468,4 @@ global.Tabletop = Tabletop; } -})(this); \ No newline at end of file +})(this);