1(function(global) { 2 "use strict"; 3 4 var inNodeJS = false; 5 if (typeof module !== 'undefined' && module.exports) { 6 inNodeJS = true; 7 var request = require('request'); 8 } 9 10 var supportsCORS = false; 11 var inLegacyIE = false; 12 try { 13 var testXHR = new XMLHttpRequest(); 14 if (typeof testXHR.withCredentials !== 'undefined') { 15 supportsCORS = true; 16 } else { 17 if ("XDomainRequest" in window) { 18 supportsCORS = true; 19 inLegacyIE = true; 20 } 21 } 22 } catch (e) { } 23 24 // Create a simple indexOf function for support 25 // of older browsers. Uses native indexOf if 26 // available. Code similar to underscores. 27 // By making a separate function, instead of adding 28 // to the prototype, we will not break bad for loops 29 // in older browsers 30 var indexOfProto = Array.prototype.indexOf; 31 var ttIndexOf = function(array, item) { 32 var i = 0, l = array.length; 33 34 if (indexOfProto && array.indexOf === indexOfProto) return array.indexOf(item); 35 for (; i < l; i++) if (array[i] === item) return i; 36 return -1; 37 }; 38 39 /* 40 Initialize with Tabletop.init( { key: '0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc' } ) 41 OR! 42 Initialize with Tabletop.init( { key: 'https://docs.google.com/spreadsheet/pub?hl=en_US&hl=en_US&key=0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc&output=html&widget=true' } ) 43 OR! 44 Initialize with Tabletop.init('0AjAPaAU9MeLFdHUxTlJiVVRYNGRJQnRmSnQwTlpoUXc') 45 */ 46 47 var Tabletop = function(options) { 48 // Make sure Tabletop is being used as a constructor no matter what. 49 if(!this || !(this instanceof Tabletop)) { 50 return new Tabletop(options); 51 } 52 53 if(typeof(options) === 'string') { 54 options = { key : options }; 55 } 56 57 this.callback = options.callback; 58 this.wanted = options.wanted || []; 59 this.key = options.key; 60 this.simpleSheet = !!options.simpleSheet; 61 this.parseNumbers = !!options.parseNumbers; 62 this.wait = !!options.wait; 63 this.reverse = !!options.reverse; 64 this.postProcess = options.postProcess; 65 this.debug = !!options.debug; 66 this.query = options.query || ''; 67 this.orderby = options.orderby; 68 this.endpoint = options.endpoint || "https://spreadsheets.google.com"; 69 this.singleton = !!options.singleton; 70 this.simple_url = !!options.simple_url; 71 this.callbackContext = options.callbackContext; 72 73 if(typeof(options.proxy) !== 'undefined') { 74 // Remove trailing slash, it will break the app 75 this.endpoint = options.proxy.replace(/\/$/,''); 76 this.simple_url = true; 77 this.singleton = true; 78 // Let's only use CORS (straight JSON request) when 79 // fetching straight from Google 80 supportsCORS = false 81 } 82 83 this.parameterize = options.parameterize || false; 84 85 if(this.singleton) { 86 if(typeof(Tabletop.singleton) !== 'undefined') { 87 this.log("WARNING! Tabletop singleton already defined"); 88 } 89 Tabletop.singleton = this; 90 } 91 92 /* Be friendly about what you accept */ 93 if(/key=/.test(this.key)) { 94 this.log("You passed an old Google Docs url as the key! Attempting to parse."); 95 this.key = this.key.match("key=(.*?)&")[1]; 96 } 97 98 if(/pubhtml/.test(this.key)) { 99 this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."); 100 this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1]; 101 } 102 103 if(!this.key) { 104 this.log("You need to pass Tabletop a key!"); 105 return; 106 } 107 108 this.log("Initializing with key " + this.key); 109 110 this.models = {}; 111 this.model_names = []; 112 113 this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; 114 115 if (inNodeJS || supportsCORS) { 116 this.base_json_path += 'json'; 117 } else { 118 this.base_json_path += 'json-in-script'; 119 } 120 121 if(!this.wait) { 122 this.fetch(); 123 } 124 }; 125 126 // A global storage for callbacks. 127 Tabletop.callbacks = {}; 128 129 // Backwards compatibility. 130 Tabletop.init = function(options) { 131 return new Tabletop(options); 132 }; 133 134 Tabletop.sheets = function() { 135 this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)"); 136 }; 137 138 Tabletop.prototype = { 139 140 fetch: function(callback) { 141 if(typeof(callback) !== "undefined") { 142 this.callback = callback; 143 } 144 this.requestData(this.base_json_path, this.loadSheets); 145 }, 146 147 /* 148 This will call the environment appropriate request method. 149 150 In browser it will use JSON-P, in node it will use request() 151 */ 152 requestData: function(path, callback) { 153 if (inNodeJS) { 154 this.serverSideFetch(path, callback); 155 } else { 156 //CORS only works in IE8/9 across the same protocol 157 //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 158 var protocol = this.endpoint.split("//").shift() || "http"; 159 if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 160 this.xhrFetch(path, callback); 161 } else { 162 this.injectScript(path, callback); 163 } 164 } 165 }, 166 167 /* 168 Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 169 */ 170 xhrFetch: function(path, callback) { 171 //support IE8's separate cross-domain object 172 var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 173 xhr.open("GET", this.endpoint + path); 174 var self = this; 175 xhr.onload = function() { 176 try { 177 var json = JSON.parse(xhr.responseText); 178 } catch (e) { 179 console.error(e); 180 } 181 callback.call(self, json); 182 }; 183 xhr.send(); 184 }, 185 186 /* 187 Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 188 it triggers the callback. This helps you avoid cross-domain errors 189 http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 190 191 Let's be plain-Jane and not use jQuery or anything. 192 */ 193 injectScript: function(path, callback) { 194 var script = document.createElement('script'); 195 var callbackName; 196 197 if(this.singleton) { 198 if(callback === this.loadSheets) { 199 callbackName = 'Tabletop.singleton.loadSheets'; 200 } else if (callback === this.loadSheet) { 201 callbackName = 'Tabletop.singleton.loadSheet'; 202 } 203 } else { 204 var self = this; 205 callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 206 // Create a temp callback which will get removed once it has executed, 207 // this allows multiple instances of Tabletop to coexist. 208 Tabletop.callbacks[ callbackName ] = function () { 209 var args = Array.prototype.slice.call( arguments, 0 ); 210 callback.apply(self, args); 211 script.parentNode.removeChild(script); 212 delete Tabletop.callbacks[callbackName]; 213 }; 214 callbackName = 'Tabletop.callbacks.' + callbackName; 215 } 216 217 var url = path + "&callback=" + callbackName; 218 219 if(this.simple_url) { 220 // We've gone down a rabbit hole of passing injectScript the path, so let's 221 // just pull the sheet_id out of the path like the least efficient worker bees 222 if(path.indexOf("/list/") !== -1) { 223 script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4]; 224 } else { 225 script.src = this.endpoint + "/" + this.key; 226 } 227 } else { 228 script.src = this.endpoint + url; 229 } 230 231 if (this.parameterize) { 232 script.src = this.parameterize + encodeURIComponent(script.src); 233 } 234 235 document.getElementsByTagName('script')[0].parentNode.appendChild(script); 236 }, 237 238 /* 239 This will only run if tabletop is being run in node.js 240 */ 241 serverSideFetch: function(path, callback) { 242 var self = this 243 request({url: this.endpoint + path, json: true}, function(err, resp, body) { 244 if (err) { 245 return console.error(err); 246 } 247 callback.call(self, body); 248 }); 249 }, 250 251 /*
252 Is this a sheet you want to pull? 253 If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 254 Pulls all sheets if none are specified 255 */ 256 isWanted: function(sheetName) { 257 if(this.wanted.length === 0) { 258 return true; 259 } else { 260 return (ttIndexOf(this.wanted, sheetName) !== -1); 261 } 262 }, 263 264 /* 265 What gets send to the callback 266 if simpleSheet === true, then don't return an array of Tabletop.this.models, 267 only return the first one's elements 268 */ 269 data: function() { 270 // If the instance is being queried before the data's been fetched 271 // then return undefined. 272 if(this.model_names.length === 0) { 273 return undefined; 274 } 275 if(this.simpleSheet) { 276 if(this.model_names.length > 1 && this.debug) { 277 this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); 278 } 279 return this.models[ this.model_names[0] ].all(); 280 } else { 281 return this.models; 282 } 283 }, 284 285 /* 286 Add another sheet to the wanted list 287 */ 288 addWanted: function(sheet) { 289 if(ttIndexOf(this.wanted, sheet) === -1) { 290 this.wanted.push(sheet); 291 } 292 }, 293 294 /* 295 Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 296 Need to use injectScript because the worksheet view that you're working from 297 doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 298 Calls back to loadSheet in order to get the real work done. 299 300 Used as a callback for the worksheet-based JSON 301 */ 302 loadSheets: function(data) { 303 var i, ilen; 304 var toLoad = []; 305 this.foundSheetNames = []; 306 307 for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 308 this.foundSheetNames.push(data.feed.entry[i].title.$t); 309 // Only pull in desired sheets to reduce loading 310 if( this.isWanted(data.feed.entry[i].content.$t) ) { 311 var linkIdx = data.feed.entry[i].link.length-1; 312 var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 313 var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=" 314 if (inNodeJS || supportsCORS) { 315 json_path += 'json'; 316 } else { 317 json_path += 'json-in-script'; 318 } 319 if(this.query) { 320 json_path += "&sq=" + this.query; 321 } 322 if(this.orderby) { 323 json_path += "&orderby=column:" + this.orderby.toLowerCase(); 324 } 325 if(this.reverse) { 326 json_path += "&reverse=true"; 327 } 328 toLoad.push(json_path); 329 } 330 } 331 332 this.sheetsToLoad = toLoad.length; 333 for(i = 0, ilen = toLoad.length; i < ilen; i++) { 334 this.requestData(toLoad[i], this.loadSheet); 335 } 336 }, 337 338 /* 339 Access layer for the this.models 340 .sheets() gets you all of the sheets 341 .sheets('Sheet1') gets you the sheet named Sheet1 342 */ 343 sheets: function(sheetName) { 344 if(typeof sheetName === "undefined") { 345 return this.models; 346 } else { 347 if(typeof(this.models[ sheetName ]) === "undefined") { 348 // alert( "Can't find " + sheetName ); 349 return; 350 } else { 351 return this.models[ sheetName ]; 352 } 353 } 354 }, 355 356 /* 357 Parse a single list-based worksheet, turning it into a Tabletop Model 358 359 Used as a callback for the list-based JSON 360 */ 361 loadSheet: function(data) { 362 var model = new Tabletop.Model( { data: data, 363 parseNumbers: this.parseNumbers, 364 postProcess: this.postProcess, 365 tabletop: this } ); 366 this.models[ model.name ] = model; 367 if(ttIndexOf(this.model_names, model.name) === -1) { 368 this.model_names.push(model.name); 369 } 370 this.sheetsToLoad--; 371 if(this.sheetsToLoad === 0) 372 this.doCallback(); 373 }, 374 375 /* 376 Execute the callback upon loading! Rely on this.data() because you might 377 only request certain pieces of data (i.e. simpleSheet mode) 378 Tests this.sheetsToLoad just in case a race condition happens to show up 379 */ 380 doCallback: function() { 381 if(this.sheetsToLoad === 0) { 382 this.callback.apply(this.callbackContext || this, [this.data(), this]); 383 } 384 }, 385 386 log: function(msg) { 387 if(this.debug) { 388 if(typeof console !== "undefined" && typeof console.log !== "undefined") { 389 Function.prototype.apply.apply(console.log, [console, arguments]); 390 } 391 } 392 } 393 394 }; 395 396 /* 397 Tabletop.Model stores the attribute names and parses the worksheet data 398 to turn it into something worthwhile 399 400 Options should be in the format { data: XXX }, with XXX being the list-based worksheet 401 */ 402 Tabletop.Model = function(options) { 403 var i, j, ilen, jlen; 404 this.column_names = []; 405 this.name = options.data.feed.title.$t; 406 this.elements = []; 407 this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 408 409 if(typeof(options.data.feed.entry) === 'undefined') { 410 options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); 411 this.elements = []; 412 return; 413 } 414 415 for(var key in options.data.feed.entry[0]){
416 if(/^gsx/.test(key)) 417 this.column_names.push( key.replace("gsx$","") ); 418 } 419 420 for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 421 var source = options.data.feed.entry[i]; 422 var element = {}; 423 for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { 424 var cell = source[ "gsx$" + this.column_names[j] ]; 425 if (typeof(cell) !== 'undefined') { 426 if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) 427 element[ this.column_names[j] ] = +cell.$t; 428 else 429 element[ this.column_names[j] ] = cell.$t; 430 } else { 431 element[ this.column_names[j] ] = ''; 432 } 433 } 434 if(element.rowNumber === undefined) 435 element.rowNumber = i + 1; 436 if( options.postProcess ) 437 options.postProcess(element); 438 this.elements.push(element); 439 } 440 441 }; 442 443 Tabletop.Model.prototype = { 444 /* 445 Returns all of the elements (rows) of the worksheet as objects 446 */ 447 all: function() { 448 return this.elements; 449 }, 450 451 /* 452 Return the elements as an array of arrays, instead of an array of objects 453 */ 454 toArray: function() { 455 var array = [], 456 i, j, ilen, jlen; 457 for(i = 0, ilen = this.elements.length; i < ilen; i++) { 458 var row = []; 459 for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 460 row.push( this.elements[i][ this.column_names[j] ] ); 461 } 462 array.push(row); 463 } 464 return array; 465 } 466 }; 467 468 if(inNodeJS) { 469 module.exports = Tabletop; 470 } else { 471 global.Tabletop = Tabletop; 472 } 473 474})(this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.