Last active
August 29, 2015 14:05
-
-
Save mvtango/ee48cd24eabe4ec40941 to your computer and use it in GitHub Desktop.
Tabletop Test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<title>Javascript Tabletop Test</title> | |
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> | |
<script src="tabletop.js"></script> | |
</head> | |
<body> | |
<div id="data"> | |
</div> | |
<script type="text/javascript"> | |
window.onload = function() { init() }; | |
var public_spreadsheet_url = 'https://docs.google.com/spreadsheets/d/1UotFPraJTw6bgKWufi53Vv7KelNLgBSoX_js4P-Ou4M/pubhtml'; | |
function init() { | |
Tabletop.init( { key: public_spreadsheet_url, | |
callback: showInfo, | |
simpleSheet: true } ) | |
} | |
function showInfo(data, tabletop) { | |
$(data).each(function(i,e) { | |
$("#data").append("<div><h1>"+e.title+"</h1><p>"+JSON.stringify(e)+"</p></div>"); | |
}); | |
} | |
</script> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Source: https://raw.githubusercontent.com/jsoma/tabletop/master/src/tabletop.js */ | |
(function(global) { | |
"use strict"; | |
var inNodeJS = false; | |
if (typeof module !== 'undefined' && module.exports) { | |
inNodeJS = true; | |
var request = require('request'); | |
} | |
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; | |
} | |
} | |
} 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' } ) | |
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.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') { | |
// 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; | |
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 an old Google Docs url as the key! Attempting to parse."); | |
this.key = this.key.match("key=(.*?)(&|#|$)")[1]; | |
} | |
if(/pubhtml/.test(this.key)) { | |
this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."); | |
this.key = this.key.match("d\\/(.*?)\\/pubhtml")[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 || supportsCORS) { | |
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 { | |
//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 | |
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 (ttIndexOf(this.wanted, 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(ttIndexOf(this.wanted, 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 linkIdx = data.feed.entry[i].link.length-1; | |
var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop(); | |
var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=" | |
if (inNodeJS || supportsCORS) { | |
json_path += 'json'; | |
} else { | |
json_path += 'json-in-script'; | |
} | |
if(this.query) { | |
json_path += "&sq=" + this.query; | |
} | |
if(this.orderby) { | |
json_path += "&orderby=column:" + this.orderby.toLowerCase(); | |
} | |
if(this.reverse) { | |
json_path += "&reverse=true"; | |
} | |
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(ttIndexOf(this.model_names, 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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment