|
(function(global) { |
|
"use strict"; |
|
|
|
var inNodeJS = false; |
|
if (typeof process !== 'undefined') { |
|
inNodeJS = true; |
|
var request = require('request'); |
|
} |
|
|
|
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf |
|
if (!Array.prototype.indexOf) { |
|
Array.prototype.indexOf = function (searchElement, fromIndex) { |
|
if (this == null) { |
|
throw new TypeError(); |
|
} |
|
var t = Object(this); |
|
var len = t.length >>> 0; |
|
if (len === 0) { |
|
return -1; |
|
} |
|
var n = 0; |
|
if (arguments.length > 1) { |
|
n = Number(arguments[1]); |
|
if (n != n) { // shortcut for verifying if it's NaN |
|
n = 0; |
|
} else if (n != 0 && n != Infinity && n != -Infinity) { |
|
n = (n > 0 || -1) * Math.floor(Math.abs(n)); |
|
} |
|
} |
|
if (n >= len) { |
|
return -1; |
|
} |
|
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); |
|
for (; k < len; k++) { |
|
if (k in t && t[k] === searchElement) { |
|
return k; |
|
} |
|
} |
|
return -1; |
|
} |
|
} |
|
/* |
|
Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) |
|
OR! |
|
Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) |
|
OR! |
|
Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') |
|
*/ |
|
|
|
var Tabletop = function(options) { |
|
// Make sure Tabletop is being used as a constructor no matter what. |
|
if(!this || !(this instanceof Tabletop)) { |
|
return new Tabletop(options); |
|
} |
|
|
|
if(typeof(options) === 'string') { |
|
options = { key : options }; |
|
} |
|
|
|
this.callback = options.callback; |
|
this.wanted = options.wanted || []; |
|
this.key = options.key; |
|
this.simpleSheet = !!options.simpleSheet; |
|
this.parseNumbers = !!options.parseNumbers; |
|
this.wait = !!options.wait; |
|
this.postProcess = options.postProcess; |
|
this.debug = !!options.debug; |
|
this.query = options.query || ''; |
|
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; |
|
this.simple_url = true; |
|
this.singleton = true; |
|
} |
|
|
|
this.parameterize = options.parameterize || false; |
|
|
|
if(this.singleton) { |
|
if(typeof(Tabletop.singleton) !== 'undefined') { |
|
this.log("WARNING! Tabletop singleton already defined"); |
|
} |
|
Tabletop.singleton = this; |
|
} |
|
|
|
/* Be friendly about what you accept */ |
|
if(/key=/.test(this.key)) { |
|
this.log("You passed a key as a URL! Attempting to parse."); |
|
this.key = this.key.match("key=(.*?)&")[1]; |
|
} |
|
|
|
if(!this.key) { |
|
this.log("You need to pass Tabletop a key!"); |
|
return; |
|
} |
|
|
|
this.log("Initializing with key " + this.key); |
|
|
|
this.models = {}; |
|
this.model_names = []; |
|
|
|
this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; |
|
|
|
if (inNodeJS) { |
|
this.base_json_path += 'json'; |
|
} else { |
|
this.base_json_path += 'json-in-script'; |
|
} |
|
|
|
if(!this.wait) { |
|
this.fetch(); |
|
} |
|
}; |
|
|
|
// A global storage for callbacks. |
|
Tabletop.callbacks = {}; |
|
|
|
// Backwards compatibility. |
|
Tabletop.init = function(options) { |
|
return new Tabletop(options); |
|
}; |
|
|
|
Tabletop.sheets = function() { |
|
this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)"); |
|
}; |
|
|
|
Tabletop.prototype = { |
|
|
|
fetch: function(callback) { |
|
if(typeof(callback) !== "undefined") { |
|
this.callback = callback; |
|
} |
|
this.requestData(this.base_json_path, this.loadSheets); |
|
}, |
|
|
|
/* |
|
This will call the environment appropriate request method. |
|
|
|
In browser it will use JSON-P, in node it will use request() |
|
*/ |
|
requestData: function(path, callback) { |
|
if (inNodeJS) { |
|
this.serverSideFetch(path, callback); |
|
} else { |
|
this.injectScript(path, callback); |
|
} |
|
}, |
|
|
|
/* |
|
Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data |
|
it triggers the callback. This helps you avoid cross-domain errors |
|
http://code.google.com/apis/gdata/samples/spreadsheet_sample.html |
|
|
|
Let's be plain-Jane and not use jQuery or anything. |
|
*/ |
|
injectScript: function(path, callback) { |
|
var script = document.createElement('script'); |
|
var callbackName; |
|
|
|
if(this.singleton) { |
|
if(callback === this.loadSheets) { |
|
callbackName = 'Tabletop.singleton.loadSheets'; |
|
} else if (callback === this.loadSheet) { |
|
callbackName = 'Tabletop.singleton.loadSheet'; |
|
} |
|
} else { |
|
var self = this; |
|
callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); |
|
// Create a temp callback which will get removed once it has executed, |
|
// this allows multiple instances of Tabletop to coexist. |
|
Tabletop.callbacks[ callbackName ] = function () { |
|
var args = Array.prototype.slice.call( arguments, 0 ); |
|
callback.apply(self, args); |
|
script.parentNode.removeChild(script); |
|
delete Tabletop.callbacks[callbackName]; |
|
}; |
|
callbackName = 'Tabletop.callbacks.' + callbackName; |
|
} |
|
|
|
var url = path + "&callback=" + callbackName; |
|
|
|
if(this.simple_url) { |
|
// We've gone down a rabbit hole of passing injectScript the path, so let's |
|
// just pull the sheet_id out of the path like the least efficient worker bees |
|
if(path.indexOf("/list/") !== -1) { |
|
script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4]; |
|
} else { |
|
script.src = this.endpoint + "/" + this.key; |
|
} |
|
} else { |
|
script.src = this.endpoint + url; |
|
} |
|
|
|
if (this.parameterize) { |
|
script.src = this.parameterize + encodeURIComponent(script.src); |
|
} |
|
|
|
document.getElementsByTagName('script')[0].parentNode.appendChild(script); |
|
}, |
|
|
|
/* |
|
This will only run if tabletop is being run in node.js |
|
*/ |
|
serverSideFetch: function(path, callback) { |
|
var self = this |
|
request({url: this.endpoint + path, json: true}, function(err, resp, body) { |
|
if (err) { |
|
return console.error(err); |
|
} |
|
callback.call(self, body); |
|
}); |
|
}, |
|
|
|
/* |
|
Is this a sheet you want to pull? |
|
If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported |
|
Pulls all sheets if none are specified |
|
*/ |
|
isWanted: function(sheetName) { |
|
if(this.wanted.length === 0) { |
|
return true; |
|
} else { |
|
return this.wanted.indexOf(sheetName) !== -1; |
|
} |
|
}, |
|
|
|
/* |
|
What gets send to the callback |
|
if simpleSheet === true, then don't return an array of Tabletop.this.models, |
|
only return the first one's elements |
|
*/ |
|
data: function() { |
|
// If the instance is being queried before the data's been fetched |
|
// then return undefined. |
|
if(this.model_names.length === 0) { |
|
return undefined; |
|
} |
|
if(this.simpleSheet) { |
|
if(this.model_names.length > 1 && this.debug) { |
|
this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); |
|
} |
|
return this.models[ this.model_names[0] ].all(); |
|
} else { |
|
return this.models; |
|
} |
|
}, |
|
|
|
/* |
|
Add another sheet to the wanted list |
|
*/ |
|
addWanted: function(sheet) { |
|
if(this.wanted.indexOf(sheet) === -1) { |
|
this.wanted.push(sheet); |
|
} |
|
}, |
|
|
|
/* |
|
Load all worksheets of the spreadsheet, turning each into a Tabletop Model. |
|
Need to use injectScript because the worksheet view that you're working from |
|
doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. |
|
Calls back to loadSheet in order to get the real work done. |
|
|
|
Used as a callback for the worksheet-based JSON |
|
*/ |
|
loadSheets: function(data) { |
|
var i, ilen; |
|
var toLoad = []; |
|
this.foundSheetNames = []; |
|
|
|
for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { |
|
this.foundSheetNames.push(data.feed.entry[i].title.$t); |
|
// Only pull in desired sheets to reduce loading |
|
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) { |
|
json_path += 'json'; |
|
} else { |
|
json_path += 'json-in-script'; |
|
} |
|
toLoad.push(json_path); |
|
} |
|
} |
|
|
|
this.sheetsToLoad = toLoad.length; |
|
for(i = 0, ilen = toLoad.length; i < ilen; i++) { |
|
this.requestData(toLoad[i], this.loadSheet); |
|
} |
|
}, |
|
|
|
/* |
|
Access layer for the this.models |
|
.sheets() gets you all of the sheets |
|
.sheets('Sheet1') gets you the sheet named Sheet1 |
|
*/ |
|
sheets: function(sheetName) { |
|
if(typeof sheetName === "undefined") { |
|
return this.models; |
|
} else { |
|
if(typeof(this.models[ sheetName ]) === "undefined") { |
|
// alert( "Can't find " + sheetName ); |
|
return; |
|
} else { |
|
return this.models[ sheetName ]; |
|
} |
|
} |
|
}, |
|
|
|
/* |
|
Parse a single list-based worksheet, turning it into a Tabletop Model |
|
|
|
Used as a callback for the list-based JSON |
|
*/ |
|
loadSheet: function(data) { |
|
var model = new Tabletop.Model( { data: data, |
|
parseNumbers: this.parseNumbers, |
|
postProcess: this.postProcess, |
|
tabletop: this } ); |
|
this.models[ model.name ] = model; |
|
if(this.model_names.indexOf(model.name) === -1) { |
|
this.model_names.push(model.name); |
|
} |
|
this.sheetsToLoad--; |
|
if(this.sheetsToLoad === 0) |
|
this.doCallback(); |
|
}, |
|
|
|
/* |
|
Execute the callback upon loading! Rely on this.data() because you might |
|
only request certain pieces of data (i.e. simpleSheet mode) |
|
Tests this.sheetsToLoad just in case a race condition happens to show up |
|
*/ |
|
doCallback: function() { |
|
if(this.sheetsToLoad === 0) { |
|
this.callback.apply(this.callbackContext || this, [this.data(), this]); |
|
} |
|
}, |
|
|
|
log: function(msg) { |
|
if(this.debug) { |
|
if(typeof console !== "undefined" && typeof console.log !== "undefined") { |
|
Function.prototype.apply.apply(console.log, [console, arguments]); |
|
} |
|
} |
|
} |
|
|
|
}; |
|
|
|
/* |
|
Tabletop.Model stores the attribute names and parses the worksheet data |
|
to turn it into something worthwhile |
|
|
|
Options should be in the format { data: XXX }, with XXX being the list-based worksheet |
|
*/ |
|
Tabletop.Model = function(options) { |
|
var i, j, ilen, jlen; |
|
this.column_names = []; |
|
this.name = options.data.feed.title.$t; |
|
this.elements = []; |
|
this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae |
|
|
|
if(typeof(options.data.feed.entry) === 'undefined') { |
|
options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); |
|
this.elements = []; |
|
return; |
|
} |
|
|
|
for(var key in options.data.feed.entry[0]){ |
|
if(/^gsx/.test(key)) |
|
this.column_names.push( key.replace("gsx$","") ); |
|
} |
|
|
|
for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { |
|
var source = options.data.feed.entry[i]; |
|
var element = {}; |
|
for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { |
|
var cell = source[ "gsx$" + this.column_names[j] ]; |
|
if (typeof(cell) !== 'undefined') { |
|
if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) |
|
element[ this.column_names[j] ] = +cell.$t; |
|
else |
|
element[ this.column_names[j] ] = cell.$t; |
|
} else { |
|
element[ this.column_names[j] ] = ''; |
|
} |
|
} |
|
if(element.rowNumber === undefined) |
|
element.rowNumber = i + 1; |
|
if( options.postProcess ) |
|
options.postProcess(element); |
|
this.elements.push(element); |
|
} |
|
|
|
}; |
|
|
|
Tabletop.Model.prototype = { |
|
/* |
|
Returns all of the elements (rows) of the worksheet as objects |
|
*/ |
|
all: function() { |
|
return this.elements; |
|
}, |
|
|
|
/* |
|
Return the elements as an array of arrays, instead of an array of objects |
|
*/ |
|
toArray: function() { |
|
var array = [], |
|
i, j, ilen, jlen; |
|
for(i = 0, ilen = this.elements.length; i < ilen; i++) { |
|
var row = []; |
|
for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { |
|
row.push( this.elements[i][ this.column_names[j] ] ); |
|
} |
|
array.push(row); |
|
} |
|
return array; |
|
} |
|
}; |
|
|
|
if(inNodeJS) { |
|
module.exports = Tabletop; |
|
} else { |
|
global.Tabletop = Tabletop; |
|
} |
|
|
|
})(this); |