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 73 function addModuleDependency(moduleName, dependentModuleName) { 74 if (!moduleDependencies[moduleName]) 75 moduleDependencies[moduleName] = []; 76 77 var dependentModules = moduleDependencies[moduleName]; 78 var found = false; 79 for (var i = 0; i < dependentModules.length; i++) 80 if (dependentModules[i] == dependentModuleName) 81 found = true; 82 if (!found) 83 dependentModules.push(dependentModuleName); 84 } 85 86 function addModuleStylesheet(moduleName, stylesheetName) { 87 if (!moduleStylesheets[moduleName]) 88 moduleStylesheets[moduleName] = []; 89 90 var stylesheets = moduleStylesheets[moduleName]; 91 var found = false; 92 for (var i = 0; i < stylesheets.length; i++) 93 if (stylesheets[i] == stylesheetName) 94 found = true; 95 if (!found) 96 stylesheets.push(stylesheetName); 97 } 98 99 function ensureDepsLoaded() { 100 if (didLoadModules) 101 return; 102 didLoadModules = true; 103 104 var req = new XMLHttpRequest(); 105 var src = moduleBasePath + '/' + 'deps.js'; 106 req.open('GET', src, false); 107 req.send(null); 108 if (req.status != 200) 109 throw new Error('Could not find ' + deps + 110 '. Run calcdeps.py and try again.'); 111 112 base.addModuleStylesheet = addModuleStylesheet; 113 base.addModuleDependency = addModuleDependency; 114 try { 115 // By construction, the deps file should call addModuleDependency. 116 eval(req.responseText); 117 } catch (e) { 118 throw new Error('When loading deps, got ' + e.stack ? e.stack : e); 119 } 120 delete base.addModuleDependency; 121 delete base.addModuleStylesheet; 122 } 123 124 var moduleLoadStatus = {}; 125 function require(dependentModuleName, opt_indentLevel) { 126 var indentLevel = opt_indentLevel || 0; 127 128 if (window.FLATTENED) { 129 if (!window.FLATTENED[dependentModuleName]) 130 throw new Error('Somehow, module ' + dependentModuleName + 131 ' didn\'t get flattened!'); 132 return; 133 } 134 ensureDepsLoaded(); 135 136 mLog('require(' + dependentModuleName + ')', indentLevel); 137 138 if (moduleLoadStatus[dependentModuleName] == 'APPENDED') 139 return; 140 if (moduleLoadStatus[dependentModuleName] == 'RESOLVING') 141 throw new Error('Circular dependency betwen modules. Cannot continue!'); 142 moduleLoadStatus[dependentModuleName] = 'RESOLVING'; 143 144 // Load the module stylesheet first. 145 var stylesheets = moduleStylesheets[dependentModuleName] || []; 146 for (var i = 0; i < stylesheets.length; i++) 147 requireStylesheet(stylesheets[i]); 148 149 // Load the module's dependent scripts after. 150 var dependentModules = 151 moduleDependencies[dependentModuleName] || []; 152 for (var i = 0; i < dependentModules.length; i++) 153 require(dependentModules[i], indentLevel + 1); 154 155 mLog('load(' + dependentModuleName + ')', indentLevel); 156 // Load the module itself. 157 var localPath = dependentModuleName.replace(/\./g, '/') + '.js'; 158 var src = moduleBasePath + '/' + localPath; 159 var text = '<script type="text/javascript" src="' + src + 160 '"></' + 'script>'; 161 base.doc.write(text); 162 moduleLoadStatus[dependentModuleName] = 'APPENDED'; 163 } 164 165 var stylesheetLoadStatus = {}; 166 function requireStylesheet(dependentStylesheetName) { 167 if (window.FLATTENED) 168 return; 169 170 if (stylesheetLoadStatus[dependentStylesheetName]) 171 return; 172 stylesheetLoadStatus[dependentStylesheetName] = true; 173 var localPath = dependentStylesheetName.replace(/\./g, '/') + '.css'; 174 var stylesheetPath = moduleBasePath + '/' + localPath; 175 176 var linkEl = document.createElement('link'); 177 linkEl.setAttribute('rel', 'stylesheet'); 178 linkEl.setAttribute('href', stylesheetPath); 179 base.doc.head.appendChild(linkEl); 180 } 181 182 function exportTo(namespace, fn) { 183 var obj = exportPath(namespace); 184 try { 185 var exports = fn(); 186 } catch (e) { 187 console.log('While running exports for ', name, ':'); 188 console.log(e.stack || e); 189 return; 190 } 191 192 for (var propertyName in exports) { 193 // Maybe we should check the prototype chain here? The current usage 194 // pattern is always using an object literal so we only care about own 195 // properties. 196 var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, 197 propertyName); 198 if (propertyDescriptor) { 199 Object.defineProperty(obj, propertyName, propertyDescriptor); 200 mLog(' +' + propertyName); 201 } 202 } 203 }; 204 205 /** 206 * Fires a property change event on the target. 207 * @param {EventTarget} target The target to dispatch the event on. 208 * @param {string} propertyName The name of the property that changed. 209 * @param {*} newValue The new value for the property. 210 * @param {*} oldValue The old value for the property. 211 */ 212 function dispatchPropertyChange(target, propertyName, newValue, oldValue) { 213 var e = new base.Event(propertyName + 'Change'); 214 e.propertyName = propertyName; 215 e.newValue = newValue; 216 e.oldValue = oldValue; 217 target.dispatchEvent(e); 218 } 219 220 /** 221 * Converts a camelCase javascript property name to a hyphenated-lower-case 222 * attribute name. 223 * @param {string} jsName The javascript camelCase property name. 224 * @return {string} The equivalent hyphenated-lower-case attribute name. 225 */ 226 function getAttributeName(jsName) { 227 return jsName.replace(/([A-Z])/g, '-$1').toLowerCase(); 228 } 229 230 /** 231 * The kind of property to define in {@code defineProperty}. 232 * @enum {number} 233 * @const 234 */ 235 var PropertyKind = { 236 /** 237 * Plain old JS property where the backing data is stored as a 'private' 238 * field on the object. 239 */ 240 JS: 'js', 241 242 /** 243 * The property backing data is stored as an attribute on an element. 244 */ 245 ATTR: 'attr', 246 247 /** 248 * The property backing data is stored as an attribute on an element. If the 249 * element has the attribute then the value is true. 250 */ 251 BOOL_ATTR: 'boolAttr' 252 }; 253 254 /** 255 * Helper function for defineProperty that returns the getter to use for the 256 * property. 257 * @param {string} name The name of the property. 258 * @param {base.PropertyKind} kind The kind of the property. 259 * @return {function():*} The getter for the property. 260 */ 261 function getGetter(name, kind) { 262 switch (kind) { 263 case PropertyKind.JS: 264 var privateName = name + '_'; 265 return function() { 266 return this[privateName]; 267 }; 268 case PropertyKind.ATTR: 269 var attributeName = getAttributeName(name); 270 return function() { 271 return this.getAttribute(attributeName); 272 }; 273 case PropertyKind.BOOL_ATTR: 274 var attributeName = getAttributeName(name); 275 return function() { 276 return this.hasAttribute(attributeName); 277 }; 278 } 279 } 280 281 /** 282 * Helper function for defineProperty that returns the setter of the right 283 * kind. 284 * @param {string} name The name of the property we are defining the setter 285 * for. 286 * @param {base.PropertyKind} kind The kind of property we are getting the 287 * setter for. 288 * @param {function(*):void} opt_setHook A function to run after the property 289 * is set, but before the propertyChange event is fired. 290 * @return {function(*):void} The function to use as a setter. 291 */ 292 function getSetter(name, kind, opt_setHook) { 293 switch (kind) { 294 case PropertyKind.JS: 295 var privateName = name + '_'; 296 return function(value) { 297 var oldValue = this[privateName]; 298 if (value !== oldValue) { 299 this[privateName] = value; 300 if (opt_setHook) 301 opt_setHook.call(this, value, oldValue); 302 dispatchPropertyChange(this, name, value, oldValue); 303 } 304 }; 305 306 case PropertyKind.ATTR: 307 var attributeName = getAttributeName(name); 308 return function(value) { 309 var oldValue = this[attributeName]; 310 if (value !== oldValue) { 311 if (value == undefined) 312 this.removeAttribute(attributeName); 313 else 314 this.setAttribute(attributeName, value); 315 if (opt_setHook) 316 opt_setHook.call(this, value, oldValue); 317 dispatchPropertyChange(this, name, value, oldValue); 318 } 319 }; 320 321 case PropertyKind.BOOL_ATTR: 322 var attributeName = getAttributeName(name); 323 return function(value) { 324 var oldValue = this[attributeName]; 325 if (value !== oldValue) { 326 if (value) 327 this.setAttribute(attributeName, name); 328 else 329 this.removeAttribute(attributeName); 330 if (opt_setHook) 331 opt_setHook.call(this, value, oldValue); 332 dispatchPropertyChange(this, name, value, oldValue); 333 } 334 }; 335 } 336 } 337 338 /** 339 * Defines a property on an object. When the setter changes the value a 340 * property change event with the type {@code name + 'Change'} is fired. 341 * @param {!Object} obj The object to define the property for. 342 * @param {string} name The name of the property. 343 * @param {base.PropertyKind=} opt_kind What kind of underlying storage to 344 * use. 345 * @param {function(*):void} opt_setHook A function to run after the 346 * property is set, but before the propertyChange event is fired. 347 */ 348 function defineProperty(obj, name, opt_kind, opt_setHook) { 349 if (typeof obj == 'function') 350 obj = obj.prototype; 351 352 var kind = opt_kind || PropertyKind.JS; 353 354 if (!obj.__lookupGetter__(name)) 355 obj.__defineGetter__(name, getGetter(name, kind)); 356 357 if (!obj.__lookupSetter__(name)) 358 obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); 359 } 360 361 /** 362 * Counter for use with createUid 363 */ 364 var uidCounter = 1; 365 366 /** 367 * @return {number} A new unique ID. 368 */ 369 function createUid() { 370 return uidCounter++; 371 } 372 373 /** 374 * Returns a unique ID for the item. This mutates the item so it needs to be 375 * an object 376 * @param {!Object} item The item to get the unique ID for. 377 * @return {number} The unique ID for the item. 378 */ 379 function getUid(item) { 380 if (item.hasOwnProperty('uid')) 381 return item.uid; 382 return item.uid = createUid(); 383 } 384 385 /** 386 * Dispatches a simple event on an event target. 387 * @param {!EventTarget} target The event target to dispatch the event on. 388 * @param {string} type The type of the event. 389 * @param {boolean=} opt_bubbles Whether the event bubbles or not. 390 * @param {boolean=} opt_cancelable Whether the default action of the event 391 * can be prevented. 392 * @return {boolean} If any of the listeners called {@code preventDefault} 393 * during the dispatch this will return false. 394 */ 395 function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { 396 var e = new base.Event(type, opt_bubbles, opt_cancelable); 397 return target.dispatchEvent(e); 398 } 399 400 /** 401 * Adds a {@code getInstance} static method that always return the same 402 * instance object. 403 * @param {!Function} ctor The constructor for the class to add the static 404 * method to. 405 */ 406 function addSingletonGetter(ctor) { 407 ctor.getInstance = function() { 408 return ctor.instance_ || (ctor.instance_ = new ctor()); 409 }; 410 } 411 412 /** 413 * Creates a new event to be used with base.EventTarget or DOM EventTarget 414 * objects. 415 * @param {string} type The name of the event. 416 * @param {boolean=} opt_bubbles Whether the event bubbles. 417 * Default is false. 418 * @param {boolean=} opt_preventable Whether the default action of the event 419 * can be prevented. 420 * @constructor 421 * @extends {Event} 422 */ 423 function Event(type, opt_bubbles, opt_preventable) { 424 var e = base.doc.createEvent('Event'); 425 e.initEvent(type, !!opt_bubbles, !!opt_preventable); 426 e.__proto__ = global.Event.prototype; 427 return e; 428 }; 429 430 /** 431 * Initialization which must be deferred until run-time. 432 */ 433 function initialize() { 434 // If 'document' isn't defined, then we must be being pre-compiled, 435 // so set a trap so that we're initialized on first access at run-time. 436 if (!global.document) { 437 var originalCr = cr; 438 439 Object.defineProperty(global, 'cr', { 440 get: function() { 441 Object.defineProperty(global, 'cr', {value: originalCr}); 442 originalBase.initialize(); 443 return originalCr; 444 }, 445 configurable: true 446 }); 447 448 return; 449 } 450 451 Event.prototype = {__proto__: global.Event.prototype}; 452 453 base.doc = document; 454 455 base.isMac = /Mac/.test(navigator.platform); 456 base.isWindows = /Win/.test(navigator.platform); 457 base.isChromeOS = /CrOS/.test(navigator.userAgent); 458 base.isLinux = /Linux/.test(navigator.userAgent); 459 base.isGTK = /GTK/.test(chrome.toolkit); 460 base.isViews = /views/.test(chrome.toolkit); 461 462 setModuleBasePath('/src'); 463 } 464 465 return { 466 set moduleBasePath(path) { 467 setModuleBasePath(path); 468 }, 469 470 get moduleBasePath() { 471 return moduleBasePath; 472 }, 473 474 require: require, 475 requireStylesheet: requireStylesheet, 476 exportTo: exportTo, 477 478 addSingletonGetter: addSingletonGetter, 479 createUid: createUid, 480 defineProperty: defineProperty, 481 dispatchPropertyChange: dispatchPropertyChange, 482 dispatchSimpleEvent: dispatchSimpleEvent, 483 Event: Event, 484 getUid: getUid, 485 initialize: initialize, 486 PropertyKind: PropertyKind 487 }; 488 })(); 489 490 491 /** 492 * TODO(kgr): Move this to another file which is to be loaded last. 493 * This will be done as part of future work to make this code pre-compilable. 494 */ 495 base.initialize(); 496