Home | History | Annotate | Download | only in src
      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