1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 'use strict'; 6 7 8 /** 9 * The global object. 10 * @type {!Object} 11 * @const 12 */ 13 var global = this; 14 15 16 /** Platform, package, object property, and Event support. */ 17 this.base = (function() { 18 19 /** 20 * Base path for modules. Used to form URLs for module 'require' requests. 21 */ 22 var moduleBasePath = '.'; 23 function setModuleBasePath(path) { 24 if (path[path.length - 1] == '/') 25 path = path.substring(0, path.length - 1); 26 moduleBasePath = path; 27 } 28 29 30 function mLog(text, opt_indentLevel) { 31 if (true) 32 return; 33 34 var spacing = ''; 35 var indentLevel = opt_indentLevel || 0; 36 for (var i = 0; i < indentLevel; i++) 37 spacing += ' '; 38 console.log(spacing + text); 39 } 40 41 /** 42 * Builds an object structure for the provided namespace path, 43 * ensuring that names that already exist are not overwritten. For 44 * example: 45 * 'a.b.c' -> a = {};a.b={};a.b.c={}; 46 * @param {string} name Name of the object that this file defines. 47 * @param {*=} opt_object The object to expose at the end of the path. 48 * @param {Object=} opt_objectToExportTo The object to add the path to; 49 * default is {@code global}. 50 * @private 51 */ 52 function exportPath(name, opt_object, opt_objectToExportTo) { 53 var parts = name.split('.'); 54 var cur = opt_objectToExportTo || global; 55 56 for (var part; parts.length && (part = parts.shift());) { 57 if (!parts.length && opt_object !== undefined) { 58 // last part and we have an object; use it 59 cur[part] = opt_object; 60 } else if (part in cur) { 61 cur = cur[part]; 62 } else { 63 cur = cur[part] = {}; 64 } 65 } 66 return cur; 67 }; 68 69 var didLoadModules = false; 70 var moduleDependencies = {}; 71 var moduleStylesheets = {}; 72 var moduleRawScripts = {}; 73 74 function addModuleDependency(moduleName, dependentModuleName) { 75 if (!moduleDependencies[moduleName]) 76 moduleDependencies[moduleName] = []; 77 78 var dependentModules = moduleDependencies[moduleName]; 79 var found = false; 80 for (var i = 0; i < dependentModules.length; i++) 81 if (dependentModules[i] == dependentModuleName) 82 found = true; 83 if (!found) 84 dependentModules.push(dependentModuleName); 85 } 86 87 function addModuleRawScriptDependency(moduleName, rawScriptName) { 88 if (!moduleRawScripts[moduleName]) 89 moduleRawScripts[moduleName] = []; 90 91 var dependentRawScripts = moduleRawScripts[moduleName]; 92 var found = false; 93 for (var i = 0; i < moduleRawScripts.length; i++) 94 if (dependentRawScripts[i] == rawScriptName) 95 found = true; 96 if (!found) 97 dependentRawScripts.push(rawScriptName); 98 } 99 100 function addModuleStylesheet(moduleName, stylesheetName) { 101 if (!moduleStylesheets[moduleName]) 102 moduleStylesheets[moduleName] = []; 103 104 var stylesheets = moduleStylesheets[moduleName]; 105 var found = false; 106 for (var i = 0; i < stylesheets.length; i++) 107 if (stylesheets[i] == stylesheetName) 108 found = true; 109 if (!found) 110 stylesheets.push(stylesheetName); 111 } 112 113 function ensureDepsLoaded() { 114 if (didLoadModules) 115 return; 116 didLoadModules = true; 117 118 var req = new XMLHttpRequest(); 119 var src = moduleBasePath + '/' + 'deps.js'; 120 req.open('GET', src, false); 121 req.send(null); 122 if (req.status != 200) 123 throw new Error('Could not find ' + src + 124 '. Run calcdeps.py and try again.'); 125 126 base.addModuleDependency = addModuleDependency; 127 base.addModuleRawScriptDependency = addModuleRawScriptDependency; 128 base.addModuleStylesheet = addModuleStylesheet; 129 try { 130 // By construction, the deps file should call addModuleDependency. 131 eval(req.responseText); 132 } catch (e) { 133 throw new Error('When loading deps, got ' + e.stack ? e.stack : e); 134 } 135 delete base.addModuleStylesheet; 136 delete base.addModuleRawScriptDependency; 137 delete base.addModuleDependency; 138 139 } 140 141 var moduleLoadStatus = {}; 142 var rawScriptLoadStatus = {}; 143 function require(dependentModuleName, opt_indentLevel) { 144 var indentLevel = opt_indentLevel || 0; 145 146 if (window.FLATTENED) { 147 if (!window.FLATTENED[dependentModuleName]) { 148 throw new Error('Somehow, module ' + dependentModuleName + 149 ' didn\'t get stored in the flattened js file! ' + 150 'You may need to rerun build/calcdeps.py'); 151 } 152 return; 153 } 154 ensureDepsLoaded(); 155 156 mLog('require(' + dependentModuleName + ')', indentLevel); 157 158 if (moduleLoadStatus[dependentModuleName] == 'APPENDED') 159 return; 160 if (moduleLoadStatus[dependentModuleName] == 'RESOLVING') 161 throw new Error('Circular dependency betwen modules. Cannot continue!'); 162 moduleLoadStatus[dependentModuleName] = 'RESOLVING'; 163 164 // Load the module stylesheet first. 165 var stylesheets = moduleStylesheets[dependentModuleName] || []; 166 for (var i = 0; i < stylesheets.length; i++) 167 requireStylesheet(stylesheets[i]); 168 169 // Load the module raw scripts next 170 var rawScripts = moduleRawScripts[dependentModuleName] || []; 171 for (var i = 0; i < rawScripts.length; i++) { 172 var rawScriptName = rawScripts[i]; 173 if (rawScriptLoadStatus[rawScriptName]) 174 continue; 175 176 mLog('load(' + rawScriptName + ')', indentLevel); 177 var src = moduleBasePath + '/' + rawScriptName; 178 var text = '<script type="text/javascript" src="' + src + 179 '"></' + 'script>'; 180 base.doc.write(text); 181 rawScriptLoadStatus[rawScriptName] = 'APPENDED'; 182 } 183 184 // Load the module's dependent scripts after. 185 var dependentModules = 186 moduleDependencies[dependentModuleName] || []; 187 for (var i = 0; i < dependentModules.length; i++) 188 require(dependentModules[i], indentLevel + 1); 189 190 mLog('load(' + dependentModuleName + ')', indentLevel); 191 // Load the module itself. 192 var localPath = dependentModuleName.replace(/\./g, '/') + '.js'; 193 var src = moduleBasePath + '/' + localPath; 194 var text = '<script type="text/javascript" src="' + src + 195 '"></' + 'script>'; 196 base.doc.write(text); 197 moduleLoadStatus[dependentModuleName] = 'APPENDED'; 198 } 199 200 /** 201 * Adds a dependency on a raw javascript file, e.g. a third party 202 * library. 203 * @param {String} rawScriptName The path to the script file, relative to 204 * moduleBasePath. 205 */ 206 function requireRawScript(rawScriptPath) { 207 if (window.FLATTENED_RAW_SCRIPTS) { 208 if (!window.FLATTENED_RAW_SCRIPTS[rawScriptPath]) { 209 throw new Error('Somehow, ' + rawScriptPath + 210 ' didn\'t get stored in the flattened js file! ' + 211 'You may need to rerun build/calcdeps.py'); 212 } 213 return; 214 } 215 216 if (rawScriptLoadStatus[rawScriptPath]) 217 return; 218 throw new Error(rawScriptPath + ' should already have been loaded.' + 219 ' Did you forget to run calcdeps.py?'); 220 } 221 222 var stylesheetLoadStatus = {}; 223 function requireStylesheet(dependentStylesheetName) { 224 if (window.FLATTENED) 225 return; 226 227 if (stylesheetLoadStatus[dependentStylesheetName]) 228 return; 229 stylesheetLoadStatus[dependentStylesheetName] = true; 230 var localPath = dependentStylesheetName.replace(/\./g, '/') + '.css'; 231 var stylesheetPath = moduleBasePath + '/' + localPath; 232 233 var linkEl = document.createElement('link'); 234 linkEl.setAttribute('rel', 'stylesheet'); 235 linkEl.setAttribute('href', stylesheetPath); 236 base.doc.head.appendChild(linkEl); 237 } 238 239 function exportTo(namespace, fn) { 240 var obj = exportPath(namespace); 241 try { 242 var exports = fn(); 243 } catch (e) { 244 console.log('While running exports for ', name, ':'); 245 console.log(e.stack || e); 246 return; 247 } 248 249 for (var propertyName in exports) { 250 // Maybe we should check the prototype chain here? The current usage 251 // pattern is always using an object literal so we only care about own 252 // properties. 253 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, 254 propertyName); 255 if (propertyDescriptor) { 256 Object.defineProperty(obj, propertyName, propertyDescriptor); 257 mLog(' +' + propertyName); 258 } 259 } 260 }; 261 262 /** 263 * Fires a property change event on the target. 264 * @param {EventTarget} target The target to dispatch the event on. 265 * @param {string} propertyName The name of the property that changed. 266 * @param {*} newValue The new value for the property. 267 * @param {*} oldValue The old value for the property. 268 */ 269 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { 270 var e = new base.Event(propertyName + 'Change'); 271 e.propertyName = propertyName; 272 e.newValue = newValue; 273 e.oldValue = oldValue; 274 target.dispatchEvent(e); 275 } 276 277 /** 278 * Converts a camelCase javascript property name to a hyphenated-lower-case 279 * attribute name. 280 * @param {string} jsName The javascript camelCase property name. 281 * @return {string} The equivalent hyphenated-lower-case attribute name. 282 */ 283 function getAttributeName(jsName) { 284 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); 285 } 286 287 /** 288 * The kind of property to define in {@code defineProperty}. 289 * @enum {number} 290 * @const 291 */ 292 var PropertyKind = { 293 /** 294 * Plain old JS property where the backing data is stored as a 'private' 295 * field on the object. 296 */ 297 JS: 'js', 298 299 /** 300 * The property backing data is stored as an attribute on an element. 301 */ 302 ATTR: 'attr', 303 304 /** 305 * The property backing data is stored as an attribute on an element. If the 306 * element has the attribute then the value is true. 307 */ 308 BOOL_ATTR: 'boolAttr' 309 }; 310 311 /** 312 * Helper function for defineProperty that returns the getter to use for the 313 * property. 314 * @param {string} name The name of the property. 315 * @param {base.PropertyKind} kind The kind of the property. 316 * @return {function():*} The getter for the property. 317 */ 318 function getGetter(name, kind) { 319 switch (kind) { 320 case PropertyKind.JS: 321 var privateName = name + '_'; 322 return function() { 323 return this[privateName]; 324 }; 325 case PropertyKind.ATTR: 326 var attributeName = getAttributeName(name); 327 return function() { 328 return this.getAttribute(attributeName); 329 }; 330 case PropertyKind.BOOL_ATTR: 331 var attributeName = getAttributeName(name); 332 return function() { 333 return this.hasAttribute(attributeName); 334 }; 335 } 336 } 337 338 /** 339 * Helper function for defineProperty that returns the setter of the right 340 * kind. 341 * @param {string} name The name of the property we are defining the setter 342 * for. 343 * @param {base.PropertyKind} kind The kind of property we are getting the 344 * setter for. 345 * @param {function(*):void} opt_setHook A function to run after the property 346 * is set, but before the propertyChange event is fired. 347 * @return {function(*):void} The function to use as a setter. 348 */ 349 function getSetter(name, kind, opt_setHook) { 350 switch (kind) { 351 case PropertyKind.JS: 352 var privateName = name + '_'; 353 return function(value) { 354 var oldValue = this[privateName]; 355 if (value !== oldValue) { 356 this[privateName] = value; 357 if (opt_setHook) 358 opt_setHook.call(this, value, oldValue); 359 dispatchPropertyChange(this, name, value, oldValue); 360 } 361 }; 362 363 case PropertyKind.ATTR: 364 var attributeName = getAttributeName(name); 365 return function(value) { 366 var oldValue = this[attributeName]; 367 if (value !== oldValue) { 368 if (value == undefined) 369 this.removeAttribute(attributeName); 370 else 371 this.setAttribute(attributeName, value); 372 if (opt_setHook) 373 opt_setHook.call(this, value, oldValue); 374 dispatchPropertyChange(this, name, value, oldValue); 375 } 376 }; 377 378 case PropertyKind.BOOL_ATTR: 379 var attributeName = getAttributeName(name); 380 return function(value) { 381 var oldValue = this[attributeName]; 382 if (value !== oldValue) { 383 if (value) 384 this.setAttribute(attributeName, name); 385 else 386 this.removeAttribute(attributeName); 387 if (opt_setHook) 388 opt_setHook.call(this, value, oldValue); 389 dispatchPropertyChange(this, name, value, oldValue); 390 } 391 }; 392 } 393 } 394 395 /** 396 * Defines a property on an object. When the setter changes the value a 397 * property change event with the type {@code name + 'Change'} is fired. 398 * @param {!Object} obj The object to define the property for. 399 * @param {string} name The name of the property. 400 * @param {base.PropertyKind=} opt_kind What kind of underlying storage to 401 * use. 402 * @param {function(*):void} opt_setHook A function to run after the 403 * property is set, but before the propertyChange event is fired. 404 */ 405 function defineProperty(obj, name, opt_kind, opt_setHook) { 406 if (typeof obj == 'function') 407 obj = obj.prototype; 408 409 var kind = opt_kind || PropertyKind.JS; 410 411 if (!obj.__lookupGetter__(name)) 412 obj.__defineGetter__(name, getGetter(name, kind)); 413 414 if (!obj.__lookupSetter__(name)) 415 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); 416 } 417 418 /** 419 * Counter for use with createUid 420 */ 421 var uidCounter = 1; 422 423 /** 424 * @return {number} A new unique ID. 425 */ 426 function createUid() { 427 return uidCounter++; 428 } 429 430 /** 431 * Returns a unique ID for the item. This mutates the item so it needs to be 432 * an object 433 * @param {!Object} item The item to get the unique ID for. 434 * @return {number} The unique ID for the item. 435 */ 436 function getUid(item) { 437 if (item.hasOwnProperty('uid')) 438 return item.uid; 439 return item.uid = createUid(); 440 } 441 442 /** 443 * Dispatches a simple event on an event target. 444 * @param {!EventTarget} target The event target to dispatch the event on. 445 * @param {string} type The type of the event. 446 * @param {boolean=} opt_bubbles Whether the event bubbles or not. 447 * @param {boolean=} opt_cancelable Whether the default action of the event 448 * can be prevented. 449 * @return {boolean} If any of the listeners called {@code preventDefault} 450 * during the dispatch this will return false. 451 */ 452 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { 453 var e = new base.Event(type, opt_bubbles, opt_cancelable); 454 return target.dispatchEvent(e); 455 } 456 457 /** 458 * Adds a {@code getInstance} static method that always return the same 459 * instance object. 460 * @param {!Function} ctor The constructor for the class to add the static 461 * method to. 462 */ 463 function addSingletonGetter(ctor) { 464 ctor.getInstance = function() { 465 return ctor.instance_ || (ctor.instance_ = new ctor()); 466 }; 467 } 468 469 /** 470 * Creates a new event to be used with base.EventTarget or DOM EventTarget 471 * objects. 472 * @param {string} type The name of the event. 473 * @param {boolean=} opt_bubbles Whether the event bubbles. 474 * Default is false. 475 * @param {boolean=} opt_preventable Whether the default action of the event 476 * can be prevented. 477 * @constructor 478 * @extends {Event} 479 */ 480 function Event(type, opt_bubbles, opt_preventable) { 481 var e = base.doc.createEvent('Event'); 482 e.initEvent(type, !!opt_bubbles, !!opt_preventable); 483 e.__proto__ = global.Event.prototype; 484 return e; 485 }; 486 487 /** 488 * Initialization which must be deferred until run-time. 489 */ 490 function initialize() { 491 // If 'document' isn't defined, then we must be being pre-compiled, 492 // so set a trap so that we're initialized on first access at run-time. 493 if (!global.document) { 494 var originalCr = cr; 495 496 Object.defineProperty(global, 'cr', { 497 get: function() { 498 Object.defineProperty(global, 'cr', {value: originalCr}); 499 originalBase.initialize(); 500 return originalCr; 501 }, 502 configurable: true 503 }); 504 505 return; 506 } 507 508 Event.prototype = {__proto__: global.Event.prototype}; 509 510 base.doc = document; 511 512 base.isMac = /Mac/.test(navigator.platform); 513 base.isWindows = /Win/.test(navigator.platform); 514 base.isChromeOS = /CrOS/.test(navigator.userAgent); 515 base.isLinux = /Linux/.test(navigator.userAgent); 516 base.isGTK = /GTK/.test(chrome.toolkit); 517 base.isViews = /views/.test(chrome.toolkit); 518 519 setModuleBasePath('/src'); 520 } 521 522 function asArray(arrayish) { 523 var values = []; 524 for (var i = 0; i < arrayish.length; i++) 525 values.push(arrayish[i]); 526 return values; 527 } 528 529 function concatenateArrays(/*arguments*/) { 530 var values = []; 531 for (var i = 0; i < arguments.length; i++) { 532 if(!(arguments[i] instanceof Array)) 533 throw new Error('Arguments ' + i + 'is not an array'); 534 values.push.apply(values, arguments[i]); 535 } 536 return values; 537 } 538 539 function dictionaryKeys(dict) { 540 var keys = []; 541 for (var key in dict) 542 keys.push(key); 543 return keys; 544 } 545 546 function dictionaryValues(dict) { 547 var values = []; 548 for (var key in dict) 549 values.push(dict[key]); 550 return values; 551 } 552 553 /** 554 * Maps types to a given value. 555 * @constructor 556 */ 557 function TypeMap() { 558 this.types = []; 559 this.values = []; 560 } 561 TypeMap.prototype = { 562 __proto__: Object.prototype, 563 564 add: function(type, value) { 565 this.types.push(type); 566 this.values.push(value); 567 }, 568 569 get: function(instance) { 570 for (var i = 0; i < this.types.length; i++) { 571 if (instance instanceof this.types[i]) 572 return this.values[i]; 573 } 574 return undefined; 575 } 576 }; 577 578 return { 579 set moduleBasePath(path) { 580 setModuleBasePath(path); 581 }, 582 583 get moduleBasePath() { 584 return moduleBasePath; 585 }, 586 587 require: require, 588 requireStylesheet: requireStylesheet, 589 requireRawScript: requireRawScript, 590 exportTo: exportTo, 591 592 addSingletonGetter: addSingletonGetter, 593 createUid: createUid, 594 defineProperty: defineProperty, 595 dispatchPropertyChange: dispatchPropertyChange, 596 dispatchSimpleEvent: dispatchSimpleEvent, 597 Event: Event, 598 getUid: getUid, 599 initialize: initialize, 600 PropertyKind: PropertyKind, 601 asArray: asArray, 602 concatenateArrays: concatenateArrays, 603 dictionaryKeys: dictionaryKeys, 604 dictionaryValues: dictionaryValues, 605 TypeMap: TypeMap, 606 }; 607 })(); 608 609 610 /** 611 * TODO(kgr): Move this to another file which is to be loaded last. 612 * This will be done as part of future work to make this code pre-compilable. 613 */ 614 base.initialize(); 615