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 // Default to on, unless there's a proxy, in which case it's default off 73 this.prettyColumnNames = typeof(options.prettyColumnNames) == 'undefined' ? !options.proxy : options.prettyColumnNames 74 75 if(typeof(options.proxy) !== 'undefined') { 76 // Remove trailing slash, it will break the app 77 this.endpoint = options.proxy.replace(/\/$/,''); 78 this.simple_url = true; 79 this.singleton = true; 80 // Let's only use CORS (straight JSON request) when 81 // fetching straight from Google 82 supportsCORS = false; 83 } 84 85 this.parameterize = options.parameterize || false; 86 87 if(this.singleton) { 88 if(typeof(Tabletop.singleton) !== 'undefined') { 89 this.log("WARNING! Tabletop singleton already defined"); 90 } 91 Tabletop.singleton = this; 92 } 93 94 /* Be friendly about what you accept */ 95 if(/key=/.test(this.key)) { 96 this.log("You passed an old Google Docs url as the key! Attempting to parse."); 97 this.key = this.key.match("key=(.*?)(&|#|$)")[1]; 98 } 99 100 if(/pubhtml/.test(this.key)) { 101 this.log("You passed a new Google Spreadsheets url as the key! Attempting to parse."); 102 this.key = this.key.match("d\\/(.*?)\\/pubhtml")[1]; 103 } 104 105 if(!this.key) { 106 this.log("You need to pass Tabletop a key!"); 107 return; 108 } 109 110 this.log("Initializing with key " + this.key); 111 112 this.models = {}; 113 this.model_names = []; 114 115 this.base_json_path = "/feeds/worksheets/" + this.key + "/public/basic?alt="; 116 117 if (inNodeJS || supportsCORS) { 118 this.base_json_path += 'json'; 119 } else { 120 this.base_json_path += 'json-in-script'; 121 } 122 123 if(!this.wait) { 124 this.fetch(); 125 } 126 }; 127 128 // A global storage for callbacks. 129 Tabletop.callbacks = {}; 130 131 // Backwards compatibility. 132 Tabletop.init = function(options) { 133 return new Tabletop(options); 134 }; 135 136 Tabletop.sheets = function() { 137 this.log("Times have changed! You'll want to use var tabletop = Tabletop.init(...); tabletop.sheets(...); instead of Tabletop.sheets(...)"); 138 }; 139 140 Tabletop.prototype = { 141 142 fetch: function(callback) { 143 if(typeof(callback) !== "undefined") { 144 this.callback = callback; 145 } 146 this.requestData(this.base_json_path, this.loadSheets); 147 }, 148 149 /* 150 This will call the environment appropriate request method. 151 152 In browser it will use JSON-P, in node it will use request() 153 */ 154 requestData: function(path, callback) { 155 if (inNodeJS) { 156 this.serverSideFetch(path, callback); 157 } else { 158 //CORS only works in IE8/9 across the same protocol 159 //You must have your server on HTTPS to talk to Google, or it'll fall back on injection 160 var protocol = this.endpoint.split("//").shift() || "http"; 161 if (supportsCORS && (!inLegacyIE || protocol === location.protocol)) { 162 this.xhrFetch(path, callback); 163 } else { 164 this.injectScript(path, callback); 165 } 166 } 167 }, 168 169 /* 170 Use Cross-Origin XMLHttpRequest to get the data in browsers that support it. 171 */ 172 xhrFetch: function(path, callback) { 173 //support IE8's separate cross-domain object 174 var xhr = inLegacyIE ? new XDomainRequest() : new XMLHttpRequest(); 175 xhr.open("GET", this.endpoint + path); 176 var self = this; 177 xhr.onload = function() { 178 try { 179 var json = JSON.parse(xhr.responseText); 180 } catch (e) { 181 console.error(e); 182 } 183 callback.call(self, json); 184 }; 185 xhr.send(); 186 }, 187 188 /* 189 Insert the URL into the page as a script tag. Once it's loaded the spreadsheet data 190 it triggers the callback. This helps you avoid cross-domain errors 191 http://code.google.com/apis/gdata/samples/spreadsheet_sample.html 192 193 Let's be plain-Jane and not use jQuery or anything. 194 */ 195 injectScript: function(path, callback) { 196 var script = document.createElement('script'); 197 var callbackName; 198 199 if(this.singleton) { 200 if(callback === this.loadSheets) { 201 callbackName = 'Tabletop.singleton.loadSheets'; 202 } else if (callback === this.loadSheet) { 203 callbackName = 'Tabletop.singleton.loadSheet'; 204 } 205 } else { 206 var self = this; 207 callbackName = 'tt' + (+new Date()) + (Math.floor(Math.random()*100000)); 208 // Create a temp callback which will get removed once it has executed, 209 // this allows multiple instances of Tabletop to coexist. 210 Tabletop.callbacks[ callbackName ] = function () { 211 var args = Array.prototype.slice.call( arguments, 0 ); 212 callback.apply(self, args); 213 script.parentNode.removeChild(script); 214 delete Tabletop.callbacks[callbackName]; 215 }; 216 callbackName = 'Tabletop.callbacks.' + callbackName; 217 } 218 219 var url = path + "&callback=" + callbackName; 220 221 if(this.simple_url) { 222 // We've gone down a rabbit hole of passing injectScript the path, so let's 223 // just pull the sheet_id out of the path like the least efficient worker bees 224 if(path.indexOf("/list/") !== -1) { 225 script.src = this.endpoint + "/" + this.key + "-" + path.split("/")[4]; 226 } else { 227 script.src = this.endpoint + "/" + this.key; 228 } 229 } else { 230 script.src = this.endpoint + url; 231 } 232 233 if (this.parameterize) { 234 script.src = this.parameterize + encodeURIComponent(script.src); 235 } 236 237 document.getElementsByTagName('script')[0].parentNode.appendChild(script); 238 }, 239 240 /* 241 This will only run if tabletop is being run in node.js 242 */ 243 serverSideFetch: function(path, callback) { 244 var self = this 245 request({url: this.endpoint + path, json: true}, function(err, resp, body) { 246 if (err) { 247 return console.error(err); 248 } 249 callback.call(self, body); 250 }); 251 }, 252 253 /*
254 Is this a sheet you want to pull? 255 If { wanted: ["Sheet1"] } has been specified, only Sheet1 is imported 256 Pulls all sheets if none are specified 257 */ 258 isWanted: function(sheetName) { 259 if(this.wanted.length === 0) { 260 return true; 261 } else { 262 return (ttIndexOf(this.wanted, sheetName) !== -1); 263 } 264 }, 265 266 /* 267 What gets send to the callback 268 if simpleSheet === true, then don't return an array of Tabletop.this.models, 269 only return the first one's elements 270 */ 271 data: function() { 272 // If the instance is being queried before the data's been fetched 273 // then return undefined. 274 if(this.model_names.length === 0) { 275 return undefined; 276 } 277 if(this.simpleSheet) { 278 if(this.model_names.length > 1 && this.debug) { 279 this.log("WARNING You have more than one sheet but are using simple sheet mode! Don't blame me when something goes wrong."); 280 } 281 return this.models[ this.model_names[0] ].all(); 282 } else { 283 return this.models; 284 } 285 }, 286 287 /* 288 Add another sheet to the wanted list 289 */ 290 addWanted: function(sheet) { 291 if(ttIndexOf(this.wanted, sheet) === -1) { 292 this.wanted.push(sheet); 293 } 294 }, 295 296 /* 297 Load all worksheets of the spreadsheet, turning each into a Tabletop Model. 298 Need to use injectScript because the worksheet view that you're working from 299 doesn't actually include the data. The list-based feed (/feeds/list/key..) does, though. 300 Calls back to loadSheet in order to get the real work done. 301 302 Used as a callback for the worksheet-based JSON 303 */ 304 loadSheets: function(data) { 305 var i, ilen; 306 var toLoad = []; 307 this.foundSheetNames = []; 308 309 for(i = 0, ilen = data.feed.entry.length; i < ilen ; i++) { 310 this.foundSheetNames.push(data.feed.entry[i].title.$t); 311 // Only pull in desired sheets to reduce loading 312 if( this.isWanted(data.feed.entry[i].content.$t) ) { 313 var linkIdx = data.feed.entry[i].link.length-1; 314 var sheet_id = data.feed.entry[i].link[linkIdx].href.split('/').pop(); 315 var json_path = "/feeds/list/" + this.key + "/" + sheet_id + "/public/values?alt=" 316 if (inNodeJS || supportsCORS) { 317 json_path += 'json'; 318 } else { 319 json_path += 'json-in-script'; 320 } 321 if(this.query) { 322 json_path += "&sq=" + this.query; 323 } 324 if(this.orderby) { 325 json_path += "&orderby=column:" + this.orderby.toLowerCase(); 326 } 327 if(this.reverse) { 328 json_path += "&reverse=true"; 329 } 330 toLoad.push(json_path); 331 } 332 } 333 334 this.sheetsToLoad = toLoad.length; 335 for(i = 0, ilen = toLoad.length; i < ilen; i++) { 336 this.requestData(toLoad[i], this.loadSheet); 337 } 338 }, 339 340 /* 341 Access layer for the this.models 342 .sheets() gets you all of the sheets 343 .sheets('Sheet1') gets you the sheet named Sheet1 344 */ 345 sheets: function(sheetName) { 346 if(typeof sheetName === "undefined") { 347 return this.models; 348 } else { 349 if(typeof(this.models[ sheetName ]) === "undefined") { 350 // alert( "Can't find " + sheetName ); 351 return; 352 } else { 353 return this.models[ sheetName ]; 354 } 355 } 356 }, 357 358 sheetReady: function(model) { 359 this.models[ model.name ] = model; 360 if(ttIndexOf(this.model_names, model.name) === -1) { 361 this.model_names.push(model.name); 362 } 363 364 this.sheetsToLoad--; 365 if(this.sheetsToLoad === 0) 366 this.doCallback(); 367 }, 368 369 /* 370 Parse a single list-based worksheet, turning it into a Tabletop Model 371 372 Used as a callback for the list-based JSON 373 */ 374 loadSheet: function(data) { 375 var that = this; 376 var model = new Tabletop.Model( { data: data, 377 parseNumbers: this.parseNumbers, 378 postProcess: this.postProcess, 379 tabletop: this, 380 prettyColumnNames: this.prettyColumnNames, 381 onReady: function() { 382 that.sheetReady(this); 383 } } ); 384 }, 385 386 /* 387 Execute the callback upon loading! Rely on this.data() because you might 388 only request certain pieces of data (i.e. simpleSheet mode) 389 Tests this.sheetsToLoad just in case a race condition happens to show up 390 */ 391 doCallback: function() { 392 if(this.sheetsToLoad === 0) { 393 this.callback.apply(this.callbackContext || this, [this.data(), this]); 394 } 395 }, 396 397 log: function(msg) { 398 if(this.debug) { 399 if(typeof console !== "undefined" && typeof console.log !== "undefined") { 400 Function.prototype.apply.apply(console.log, [console, arguments]); 401 } 402 } 403 } 404 405 }; 406 407 /* 408 Tabletop.Model stores the attribute names and parses the worksheet data 409 to turn it into something worthwhile 410 411 Options should be in the format { data: XXX }, with XXX being the list-based worksheet 412 */ 413 Tabletop.Model = function(options) { 414 var i, j, ilen, jlen; 415 this.column_names = []; 416 this.name = options.data.feed.title.$t; 417 this.tabletop = options.tabletop; 418 this.elements = []; 419 this.onReady = options.onReady; 420 this.raw = options.data; // A copy of the sheet's raw data, for accessing minutiae 421 422 if(typeof(options.data.feed.entry) === 'undefined') { 423 options.tabletop.log("Missing data for " + this.name + ", make sure you didn't forget column headers"); 424 this.original_columns = []; 425 this.elements = []; 426 this.onReady.call(this); 427 return; 428 } 429 430 for(var key in options.data.feed.entry[0]){
431 if(/^gsx/.test(key)) 432 this.column_names.push( key.replace("gsx$","") ); 433 } 434 435 this.original_columns = this.column_names; 436 437 for(i = 0, ilen = options.data.feed.entry.length ; i < ilen; i++) { 438 var source = options.data.feed.entry[i]; 439 var element = {}; 440 for(var j = 0, jlen = this.column_names.length; j < jlen ; j++) { 441 var cell = source[ "gsx$" + this.column_names[j] ]; 442 if (typeof(cell) !== 'undefined') { 443 if(options.parseNumbers && cell.$t !== '' && !isNaN(cell.$t)) 444 element[ this.column_names[j] ] = +cell.$t; 445 else 446 element[ this.column_names[j] ] = cell.$t; 447 } else { 448 element[ this.column_names[j] ] = ''; 449 } 450 } 451 if(element.rowNumber === undefined) 452 element.rowNumber = i + 1; 453 if( options.postProcess ) 454 options.postProcess(element); 455 this.elements.push(element); 456 } 457 458 if(options.prettyColumnNames) 459 this.fetchPrettyColumns(); 460 else 461 this.onReady.call(this); 462 }; 463 464 Tabletop.Model.prototype = { 465 /* 466 Returns all of the elements (rows) of the worksheet as objects 467 */ 468 all: function() { 469 return this.elements; 470 }, 471 472 fetchPrettyColumns: function() { 473 if(!this.raw.feed.link[3]) 474 return this.ready(); 475 var cellurl = this.raw.feed.link[3].href.replace('/feeds/list/', '/feeds/cells/').replace('https://spreadsheets.google.com', ''); 476 var that = this; 477 this.tabletop.requestData(cellurl, function(data) { 478 that.loadPrettyColumns(data) 479 }); 480 }, 481 482 ready: function() { 483 this.onReady.call(this); 484 }, 485 486 /* 487 * Store column names as an object 488 * with keys of Google-formatted "columnName" 489 * and values of human-readable "Column name" 490 */ 491 loadPrettyColumns: function(data) { 492 var pretty_columns = {}; 493 494 var column_names = this.column_names; 495 496 var i = 0; 497 var l = column_names.length; 498 499 for (; i < l; i++) { 500 if (typeof data.feed.entry[i].content.$t !== 'undefined') { 501 pretty_columns[column_names[i]] = data.feed.entry[i].content.$t; 502 } else { 503 pretty_columns[column_names[i]] = column_names[i]; 504 } 505 } 506 507 this.pretty_columns = pretty_columns; 508 509 this.prettifyElements(); 510 this.ready(); 511 }, 512 513 /* 514 * Go through each row, substitutiting 515 * Google-formatted "columnName" 516 * with human-readable "Column name" 517 */ 518 prettifyElements: function() { 519 var pretty_elements = [], 520 ordered_pretty_names = [], 521 i, j, ilen, jlen; 522 523 var ordered_pretty_names; 524 for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 525 ordered_pretty_names.push(this.pretty_columns[this.column_names[j]]); 526 } 527 528 for(i = 0, ilen = this.elements.length; i < ilen; i++) { 529 var new_element = {}; 530 for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 531 var new_column_name = this.pretty_columns[this.column_names[j]]; 532 new_element[new_column_name] = this.elements[i][this.column_names[j]]; 533 } 534 pretty_elements.push(new_element); 535 } 536 this.elements = pretty_elements; 537 this.column_names = ordered_pretty_names; 538 }, 539 540 /* 541 Return the elements as an array of arrays, instead of an array of objects 542 */ 543 toArray: function() { 544 var array = [], 545 i, j, ilen, jlen; 546 for(i = 0, ilen = this.elements.length; i < ilen; i++) { 547 var row = []; 548 for(j = 0, jlen = this.column_names.length; j < jlen ; j++) { 549 row.push( this.elements[i][ this.column_names[j] ] ); 550 } 551 array.push(row); 552 } 553 return array; 554 } 555 }; 556 557 if(inNodeJS) { 558 module.exports = Tabletop; 559 } else if (typeof define === 'function' && define.amd) { 560 define(function () { 561 return Tabletop; 562 }); 563 } else { 564 global.Tabletop = Tabletop; 565 } 566 567})(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.