[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/webroot/rsrc/externals/javelin/core/ -> install.js (source)

   1  /**
   2   * @requires javelin-util
   3   *           javelin-magical-init
   4   * @provides javelin-install
   5   *
   6   * @javelin-installs JX.install
   7   * @javelin-installs JX.createClass
   8   *
   9   * @javelin
  10   */
  11  
  12  /**
  13   * Install a class into the Javelin ("JX") namespace. The first argument is the
  14   * name of the class you want to install, and the second is a map of these
  15   * attributes (all of which are optional):
  16   *
  17   *   - ##construct## //(function)// Class constructor. If you don't provide one,
  18   *       one will be created for you (but it will be very boring).
  19   *   - ##extend## //(string)// The name of another JX-namespaced class to extend
  20   *       via prototypal inheritance.
  21   *   - ##members## //(map)// A map of instance methods and properties.
  22   *   - ##statics## //(map)// A map of static methods and properties.
  23   *   - ##initialize## //(function)// A function which will be run once, after
  24   *       this class has been installed.
  25   *   - ##properties## //(map)// A map of properties that should have instance
  26   *       getters and setters automatically generated for them. The key is the
  27   *       property name and the value is its default value. For instance, if you
  28   *       provide the property "size", the installed class will have the methods
  29   *       "getSize()" and "setSize()". It will **NOT** have a property ".size"
  30   *       and no guarantees are made about where install is actually chosing to
  31   *       store the data. The motivation here is to let you cheaply define a
  32   *       stable interface and refine it later as necessary.
  33   *   - ##events## //(list)// List of event types this class is capable of
  34   *       emitting.
  35   *
  36   * For example:
  37   *
  38   *   JX.install('Dog', {
  39   *     construct : function(name) {
  40   *       this.setName(name);
  41   *     },
  42   *     members : {
  43   *       bark : function() {
  44   *         // ...
  45   *       }
  46   *     },
  47   *     properites : {
  48   *       name : null,
  49   *     }
  50   *   });
  51   *
  52   * This creates a new ##Dog## class in the ##JX## namespace:
  53   *
  54   *   var d = new JX.Dog();
  55   *   d.bark();
  56   *
  57   * Javelin classes are normal Javascript functions and generally behave in
  58   * the expected way. Some properties and methods are automatically added to
  59   * all classes:
  60   *
  61   *   - ##instance.__id__## Globally unique identifier attached to each instance.
  62   *   - ##prototype.__class__## Reference to the class constructor.
  63   *   - ##constructor.__path__## List of path tokens used emit events. It is
  64   *       probably never useful to access this directly.
  65   *   - ##constructor.__readable__## Readable class name. You could use this
  66   *       for introspection.
  67   *   - ##constructor.__events__## //DEV ONLY!// List of events supported by
  68   *       this class.
  69   *   - ##constructor.listen()## Listen to all instances of this class. See
  70   *       @{JX.Base}.
  71   *   - ##instance.listen()## Listen to one instance of this class. See
  72   *       @{JX.Base}.
  73   *   - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}.
  74   *
  75   *
  76   * @param  string  Name of the class to install. It will appear in the JX
  77   *                 "namespace" (e.g., JX.Pancake).
  78   * @param  map     Map of properties, see method documentation.
  79   * @return void
  80   */
  81  JX.install = function(new_name, new_junk) {
  82  
  83    // If we've already installed this, something is up.
  84    if (new_name in JX) {
  85      if (__DEV__) {
  86        JX.$E(
  87          'JX.install("' + new_name + '", ...): ' +
  88          'trying to reinstall something that has already been installed.');
  89      }
  90      return;
  91    }
  92  
  93    if (__DEV__) {
  94      if ('name' in new_junk) {
  95        JX.$E(
  96          'JX.install("' + new_name + '", {"name": ...}): ' +
  97          'trying to install with "name" property.' +
  98          'Either remove it or call JX.createClass directly.');
  99      }
 100    }
 101  
 102    // Since we may end up loading things out of order (e.g., Dog extends Animal
 103    // but we load Dog first) we need to keep a list of things that we've been
 104    // asked to install but haven't yet been able to install around.
 105    (JX.install._queue || (JX.install._queue = [])).push([new_name, new_junk]);
 106    var name;
 107    do {
 108      var junk;
 109      var initialize;
 110      name = null;
 111      for (var ii = 0; ii < JX.install._queue.length; ++ii) {
 112        junk = JX.install._queue[ii][1];
 113        if (junk.extend && !JX[junk.extend]) {
 114          // We need to extend something that we haven't been able to install
 115          // yet, so just keep this in queue.
 116          continue;
 117        }
 118  
 119        // Install time! First, get this out of the queue.
 120        name = JX.install._queue.splice(ii, 1)[0][0];
 121        --ii;
 122  
 123        if (junk.extend) {
 124          junk.extend = JX[junk.extend];
 125        }
 126  
 127        initialize = junk.initialize;
 128        delete junk.initialize;
 129        junk.name = 'JX.' + name;
 130  
 131        JX[name] = JX.createClass(junk);
 132  
 133        if (initialize) {
 134          if (JX['Stratcom'] && JX['Stratcom'].ready) {
 135            initialize.apply(null);
 136          } else {
 137            // This is a holding queue, defined in init.js.
 138            JX['install-init'](initialize);
 139          }
 140        }
 141      }
 142  
 143      // In effect, this exits the loop as soon as we didn't make any progress
 144      // installing things, which means we've installed everything we have the
 145      // dependencies for.
 146    } while (name);
 147  };
 148  
 149  /**
 150   * Creates a class from a map of attributes. Requires ##extend## property to
 151   * be an actual Class object and not a "String". Supports ##name## property
 152   * to give the created Class a readable name.
 153   *
 154   * @see JX.install for description of supported attributes.
 155   *
 156   * @param  junk     Map of properties, see method documentation.
 157   * @return function Constructor of a class created
 158   */
 159  JX.createClass = function(junk) {
 160    var name = junk.name || '';
 161    var k;
 162    var ii;
 163  
 164    if (__DEV__) {
 165      var valid = {
 166        construct : 1,
 167        statics : 1,
 168        members : 1,
 169        extend : 1,
 170        properties : 1,
 171        events : 1,
 172        name : 1
 173      };
 174      for (k in junk) {
 175        if (!(k in valid)) {
 176          JX.$E(
 177            'JX.createClass("' + name + '", {"' + k + '": ...}): ' +
 178            'trying to create unknown property `' + k + '`.');
 179        }
 180      }
 181      if (junk.constructor !== {}.constructor) {
 182        JX.$E(
 183          'JX.createClass("' + name + '", {"constructor": ...}): ' +
 184          'property `constructor` should be called `construct`.');
 185      }
 186    }
 187  
 188    // First, build the constructor. If construct is just a function, this
 189    // won't change its behavior (unless you have provided a really awesome
 190    // function, in which case it will correctly punish you for your attempt
 191    // at creativity).
 192    var Class = (function(name, junk) {
 193      var result = function() {
 194        this.__id__ = '__obj__' + (++JX.install._nextObjectID);
 195        return (junk.construct || junk.extend || JX.bag).apply(this, arguments);
 196        // TODO: Allow mixins to initialize here?
 197        // TODO: Also, build mixins?
 198      };
 199  
 200      if (__DEV__) {
 201        var inner = result;
 202        result = function() {
 203          if (this == window || this == JX) {
 204            JX.$E(
 205              '<' + Class.__readable__ + '>: ' +
 206              'Tried to construct an instance without the "new" operator.');
 207          }
 208          return inner.apply(this, arguments);
 209        };
 210      }
 211      return result;
 212    })(name, junk);
 213  
 214    Class.__readable__ = name;
 215  
 216    // Copy in all the static methods and properties.
 217    for (k in junk.statics) {
 218      // Can't use JX.copy() here yet since it may not have loaded.
 219      Class[k] = junk.statics[k];
 220    }
 221  
 222    var proto;
 223    if (junk.extend) {
 224      var Inheritance = function() {};
 225      Inheritance.prototype = junk.extend.prototype;
 226      proto = Class.prototype = new Inheritance();
 227    } else {
 228      proto = Class.prototype = {};
 229    }
 230  
 231    proto.__class__ = Class;
 232    var setter = function(prop) {
 233      return function(v) {
 234        this[prop] = v;
 235        return this;
 236      };
 237    };
 238    var getter = function(prop) {
 239      return function(v) {
 240        return this[prop];
 241      };
 242    };
 243  
 244    // Build getters and setters from the `prop' map.
 245    for (k in (junk.properties || {})) {
 246      var base = k.charAt(0).toUpperCase() + k.substr(1);
 247      var prop = '__auto__' + k;
 248      proto[prop] = junk.properties[k];
 249      proto['set' + base] = setter(prop);
 250      proto['get' + base] = getter(prop);
 251    }
 252  
 253    if (__DEV__) {
 254  
 255      // Check for aliasing in default values of members. If we don't do this,
 256      // you can run into a problem like this:
 257      //
 258      //  JX.install('List', { members : { stuff : [] }});
 259      //
 260      //  var i_love = new JX.List();
 261      //  var i_hate = new JX.List();
 262      //
 263      //  i_love.stuff.push('Psyduck');  // I love psyduck!
 264      //  JX.log(i_hate.stuff);          // Show stuff I hate.
 265      //
 266      // This logs ["Psyduck"] because the push operation modifies
 267      // JX.List.prototype.stuff, which is what both i_love.stuff and
 268      // i_hate.stuff resolve to. To avoid this, set the default value to
 269      // null (or any other scalar) and do "this.stuff = [];" in the
 270      // constructor.
 271  
 272      for (var member_name in junk.members) {
 273        if (junk.extend && member_name[0] == '_') {
 274          JX.$E(
 275            'JX.createClass("' + name + '", ...): ' +
 276            'installed member "' + member_name + '" must not be named with ' +
 277            'a leading underscore because it is in a subclass. Variables ' +
 278            'are analyzed and crushed one file at a time, and crushed ' +
 279            'member variables in subclasses alias crushed member variables ' +
 280            'in superclasses. Remove the underscore, refactor the class so ' +
 281            'it does not extend anything, or fix the minifier to be ' +
 282            'capable of safely crushing subclasses.');
 283        }
 284        var member_value = junk.members[member_name];
 285        if (typeof member_value == 'object' && member_value !== null) {
 286          JX.$E(
 287            'JX.createClass("' + name + '", ...): ' +
 288            'installed member "' + member_name + '" is not a scalar or ' +
 289            'function. Prototypal inheritance in Javascript aliases object ' +
 290            'references across instances so all instances are initialized ' +
 291            'to point at the exact same object. This is almost certainly ' +
 292            'not what you intended. Make this member static to share it ' +
 293            'across instances, or initialize it in the constructor to ' +
 294            'prevent reference aliasing and give each instance its own ' +
 295            'copy of the value.');
 296        }
 297      }
 298    }
 299  
 300  
 301    // This execution order intentionally allows you to override methods
 302    // generated from the "properties" initializer.
 303    for (k in junk.members) {
 304      proto[k] = junk.members[k];
 305    }
 306  
 307    // IE does not enumerate some properties on objects
 308    var enumerables = JX.install._enumerables;
 309    if (junk.members && enumerables) {
 310      ii = enumerables.length;
 311      while (ii--){
 312        var property = enumerables[ii];
 313        if (junk.members[property]) {
 314          proto[property] = junk.members[property];
 315        }
 316      }
 317    }
 318  
 319    // Build this ridiculous event model thing. Basically, this defines
 320    // two instance methods, invoke() and listen(), and one static method,
 321    // listen(). If you listen to an instance you get events for that
 322    // instance; if you listen to a class you get events for all instances
 323    // of that class (including instances of classes which extend it).
 324    //
 325    // This is rigged up through Stratcom. Each class has a path component
 326    // like "class:Dog", and each object has a path component like
 327    // "obj:23". When you invoke on an object, it emits an event with
 328    // a path that includes its class, all parent classes, and its object
 329    // ID.
 330    //
 331    // Calling listen() on an instance listens for just the object ID.
 332    // Calling listen() on a class listens for that class's name. This
 333    // has the effect of working properly, but installing them is pretty
 334    // messy.
 335  
 336    var parent = junk.extend || {};
 337    var old_events = parent.__events__;
 338    var new_events = junk.events || [];
 339    var has_events = old_events || new_events.length;
 340  
 341    if (has_events) {
 342      var valid_events = {};
 343  
 344      // If we're in dev, we build up a list of valid events (for this class
 345      // and our parent class), and then check them on listen and invoke.
 346      if (__DEV__) {
 347        for (var key in old_events || {}) {
 348          valid_events[key] = true;
 349        }
 350        for (ii = 0; ii < new_events.length; ++ii) {
 351          valid_events[junk.events[ii]] = true;
 352        }
 353      }
 354  
 355      Class.__events__ = valid_events;
 356  
 357      // Build the class name chain.
 358      Class.__name__ = 'class:' + name;
 359      var ancestry = parent.__path__ || [];
 360      Class.__path__ = ancestry.concat([Class.__name__]);
 361  
 362      proto.invoke = function(type) {
 363        if (__DEV__) {
 364          if (!(type in this.__class__.__events__)) {
 365            JX.$E(
 366              this.__class__.__readable__ + '.invoke("' + type + '", ...): ' +
 367              'invalid event type. Valid event types are: ' +
 368              JX.keys(this.__class__.__events__).join(', ') + '.');
 369          }
 370        }
 371        // Here and below, this nonstandard access notation is used to mask
 372        // these callsites from the static analyzer. JX.Stratcom is always
 373        // available by the time we hit these execution points.
 374        return JX['Stratcom'].invoke(
 375          'obj:' + type,
 376          this.__class__.__path__.concat([this.__id__]),
 377          {args : JX.$A(arguments).slice(1)});
 378      };
 379  
 380      proto.listen = function(type, callback) {
 381        if (__DEV__) {
 382          if (!(type in this.__class__.__events__)) {
 383            JX.$E(
 384              this.__class__.__readable__ + '.listen("' + type + '", ...): ' +
 385              'invalid event type. Valid event types are: ' +
 386              JX.keys(this.__class__.__events__).join(', ') + '.');
 387          }
 388        }
 389        return JX['Stratcom'].listen(
 390          'obj:' + type,
 391          this.__id__,
 392          JX.bind(this, function(e) {
 393            return callback.apply(this, e.getData().args);
 394          }));
 395      };
 396  
 397      Class.listen = function(type, callback) {
 398        if (__DEV__) {
 399          if (!(type in this.__events__)) {
 400            JX.$E(
 401              this.__readable__ + '.listen("' + type + '", ...): ' +
 402              'invalid event type. Valid event types are: ' +
 403              JX.keys(this.__events__).join(', ') + '.');
 404          }
 405        }
 406        return JX['Stratcom'].listen(
 407          'obj:' + type,
 408          this.__name__,
 409          JX.bind(this, function(e) {
 410            return callback.apply(this, e.getData().args);
 411          }));
 412      };
 413    } else if (__DEV__) {
 414      var error_message =
 415        'class does not define any events. Pass an "events" property to ' +
 416        'JX.createClass() to define events.';
 417      Class.listen = Class.listen || function() {
 418        JX.$E(
 419          this.__readable__ + '.listen(...): ' +
 420          error_message);
 421      };
 422      Class.invoke = Class.invoke || function() {
 423        JX.$E(
 424          this.__readable__ + '.invoke(...): ' +
 425          error_message);
 426      };
 427      proto.listen = proto.listen || function() {
 428        JX.$E(
 429          this.__class__.__readable__ + '.listen(...): ' +
 430          error_message);
 431      };
 432      proto.invoke = proto.invoke || function() {
 433        JX.$E(
 434          this.__class__.__readable__ + '.invoke(...): ' +
 435          error_message);
 436      };
 437    }
 438  
 439    return Class;
 440  };
 441  
 442  JX.install._nextObjectID = 0;
 443  JX.flushHoldingQueue('install', JX.install);
 444  
 445  (function() {
 446    // IE does not enter this loop.
 447    for (var i in {toString: 1}) {
 448      return;
 449    }
 450  
 451    JX.install._enumerables = [
 452      'toString', 'hasOwnProperty', 'valueOf', 'isPrototypeOf',
 453      'propertyIsEnumerable', 'toLocaleString', 'constructor'
 454    ];
 455  })();


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1