414 lines
13 KiB
JavaScript
414 lines
13 KiB
JavaScript
|
(function(global) {
|
||
|
"use strict";
|
||
|
|
||
|
var inNodeJS = false;
|
||
|
if (typeof process !== 'undefined') {
|
||
|
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);
|
||
|
}
|
||
|
for (var i = fromIndex, j = this.length; i < j; i++) {
|
||
|
if (this[i] === obj) {
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
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);
|