[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

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

   1  /**
   2   * @requires javelin-install javelin-event javelin-util javelin-magical-init
   3   * @provides javelin-stratcom
   4   * @javelin
   5   */
   6  
   7  /**
   8   * Javelin strategic command, the master event delegation core. This class is
   9   * a sort of hybrid between Arbiter and traditional event delegation, and
  10   * serves to route event information to handlers in a general way.
  11   *
  12   * Each Javelin :JX.Event has a 'type', which may be a normal Javascript type
  13   * (for instance, a click or a keypress) or an application-defined type. It
  14   * also has a "path", based on the path in the DOM from the root node to the
  15   * event target. Note that, while the type is required, the path may be empty
  16   * (it often will be for application-defined events which do not originate
  17   * from the DOM).
  18   *
  19   * The path is determined by walking down the tree to the event target and
  20   * looking for nodes that have been tagged with metadata. These names are used
  21   * to build the event path, and unnamed nodes are ignored. Each named node may
  22   * also have data attached to it.
  23   *
  24   * Listeners specify one or more event types they are interested in handling,
  25   * and, optionally, one or more paths. A listener will only receive events
  26   * which occurred on paths it is listening to. See listen() for more details.
  27   *
  28   * @task invoke   Invoking Events
  29   * @task listen   Listening to Events
  30   * @task handle   Responding to Events
  31   * @task sigil    Managing Sigils
  32   * @task meta     Managing Metadata
  33   * @task internal Internals
  34   */
  35  JX.install('Stratcom', {
  36    statics : {
  37      ready : false,
  38      _targets : {},
  39      _handlers : [],
  40      _need : {},
  41      _auto : '*',
  42      _data : {},
  43      _execContext : [],
  44  
  45      /**
  46       * Node metadata is stored in a series of blocks to prevent collisions
  47       * between indexes that are generated on the server side (and potentially
  48       * concurrently). Block 0 is for metadata on the initial page load, block 1
  49       * is for metadata added at runtime with JX.Stratcom.siglize(), and blocks
  50       * 2 and up are for metadata generated from other sources (e.g. JX.Request).
  51       * Use allocateMetadataBlock() to reserve a block, and mergeData() to fill
  52       * a block with data.
  53       *
  54       * When a JX.Request is sent, a block is allocated for it and any metadata
  55       * it returns is filled into that block.
  56       */
  57      _dataBlock : 2,
  58  
  59      /**
  60       * Within each datablock, data is identified by a unique index. The data
  61       * pointer (data-meta attribute) on a node looks like this:
  62       *
  63       *  1_2
  64       *
  65       * ...where 1 is the block, and 2 is the index within that block. Normally,
  66       * blocks are filled on the server side, so index allocation takes place
  67       * there. However, when data is provided with JX.Stratcom.addData(), we
  68       * need to allocate indexes on the client.
  69       */
  70      _dataIndex : 0,
  71  
  72      /**
  73       * Dispatch a simple event that does not have a corresponding native event
  74       * object. It is unusual to call this directly. Generally, you will instead
  75       * dispatch events from an object using the invoke() method present on all
  76       * objects. See @{JX.Base.invoke()} for documentation.
  77       *
  78       * @param  string       Event type.
  79       * @param  string|list? Optionally, a sigil path to attach to the event.
  80       *                      This is rarely meaningful for simple events.
  81       * @param  object?      Optionally, arbitrary data to send with the event.
  82       * @return @{JX.Event}  The event object which was dispatched to listeners.
  83       *                      The main use of this is to test whether any
  84       *                      listeners prevented the event.
  85       * @task invoke
  86       */
  87      invoke : function(type, path, data) {
  88        if (__DEV__) {
  89          if (path && typeof path !== 'string' && !JX.isArray(path)) {
  90            throw new Error(
  91              'JX.Stratcom.invoke(...): path must be a string or an array.');
  92          }
  93        }
  94  
  95        path = JX.$AX(path);
  96  
  97        return this._dispatchProxy(
  98          new JX.Event()
  99            .setType(type)
 100            .setData(data || {})
 101            .setPath(path || [])
 102        );
 103      },
 104  
 105  
 106      /**
 107       * Listen for events on given paths. Specify one or more event types, and
 108       * zero or more paths to filter on. If you don't specify a path, you will
 109       * receive all events of the given type:
 110       *
 111       *   // Listen to all clicks.
 112       *   JX.Stratcom.listen('click', null, handler);
 113       *
 114       * This will notify you of all clicks anywhere in the document (unless
 115       * they are intercepted and killed by a higher priority handler before they
 116       * get to you).
 117       *
 118       * Often, you may be interested in only clicks on certain elements. You
 119       * can specify the paths you're interested in to filter out events which
 120       * you do not want to be notified of.
 121       *
 122       *   //  Listen to all clicks inside elements annotated "news-feed".
 123       *   JX.Stratcom.listen('click', 'news-feed', handler);
 124       *
 125       * By adding more elements to the path, you can create a finer-tuned
 126       * filter:
 127       *
 128       *   //  Listen to only "like" clicks inside "news-feed".
 129       *   JX.Stratcom.listen('click', ['news-feed', 'like'], handler);
 130       *
 131       *
 132       * TODO: Further explain these shenanigans.
 133       *
 134       * @param  string|list<string>  Event type (or list of event names) to
 135       *                   listen for. For example, ##'click'## or
 136       *                   ##['keydown', 'keyup']##.
 137       *
 138       * @param  wild      Sigil paths to listen for this event on. See discussion
 139       *                   in method documentation.
 140       *
 141       * @param  function  Callback to invoke when this event is triggered. It
 142       *                   should have the signature ##f(:JX.Event e)##.
 143       *
 144       * @return object    A reference to the installed listener. You can later
 145       *                   remove the listener by calling this object's remove()
 146       *                   method.
 147       * @task listen
 148       */
 149      listen : function(types, paths, func) {
 150  
 151        if (__DEV__) {
 152          if (arguments.length != 3) {
 153            JX.$E(
 154              'JX.Stratcom.listen(...): '+
 155              'requires exactly 3 arguments. Did you mean JX.DOM.listen?');
 156          }
 157          if (typeof func != 'function') {
 158            JX.$E(
 159              'JX.Stratcom.listen(...): '+
 160              'callback is not a function.');
 161          }
 162        }
 163  
 164        var ids = [];
 165  
 166        types = JX.$AX(types);
 167  
 168        if (!paths) {
 169          paths = this._auto;
 170        }
 171        if (!JX.isArray(paths)) {
 172          paths = [[paths]];
 173        } else if (!JX.isArray(paths[0])) {
 174          paths = [paths];
 175        }
 176  
 177        var listener = { _callback : func };
 178  
 179        //  To listen to multiple event types on multiple paths, we just install
 180        //  the same listener a whole bunch of times: if we install for two
 181        //  event types on three paths, we'll end up with six references to the
 182        //  listener.
 183        //
 184        //  TODO: we'll call your listener twice if you install on two paths where
 185        //  one path is a subset of another. The solution is "don't do that", but
 186        //  it would be nice to verify that the caller isn't doing so, in __DEV__.
 187        for (var ii = 0; ii < types.length; ++ii) {
 188          var type = types[ii];
 189          if (('onpagehide' in window) && type == 'unload') {
 190            // If we use "unload", we break the bfcache ("Back-Forward Cache") in
 191            // Safari and Firefox. The BFCache makes using the back/forward
 192            // buttons really fast since the pages can come out of magical
 193            // fairyland instead of over the network, so use "pagehide" as a proxy
 194            // for "unload" in these browsers.
 195            type = 'pagehide';
 196          }
 197          if (!(type in this._targets)) {
 198            this._targets[type] = {};
 199          }
 200          var type_target = this._targets[type];
 201          for (var jj = 0; jj < paths.length; ++jj) {
 202            var path = paths[jj];
 203            var id = this._handlers.length;
 204            this._handlers.push(listener);
 205            this._need[id] = path.length;
 206            ids.push(id);
 207            for (var kk = 0; kk < path.length; ++kk) {
 208              if (__DEV__) {
 209                if (path[kk] == 'tag:#document') {
 210                  JX.$E(
 211                    'JX.Stratcom.listen(..., "tag:#document", ...): ' +
 212                    'listen for all events using null, not "tag:#document"');
 213                }
 214                if (path[kk] == 'tag:window') {
 215                  JX.$E(
 216                    'JX.Stratcom.listen(..., "tag:window", ...): ' +
 217                    'listen for window events using null, not "tag:window"');
 218                }
 219              }
 220              (type_target[path[kk]] || (type_target[path[kk]] = [])).push(id);
 221            }
 222          }
 223        }
 224  
 225        // Add a remove function to the listener
 226        listener['remove'] = function() {
 227          if (listener._callback) {
 228            delete listener._callback;
 229            for (var ii = 0; ii < ids.length; ii++) {
 230              delete JX.Stratcom._handlers[ids[ii]];
 231            }
 232          }
 233        };
 234  
 235        return listener;
 236      },
 237  
 238  
 239      /**
 240       * Sometimes you may be interested in removing a listener directly from it's
 241       * handler. This is possible by calling JX.Stratcom.removeCurrentListener()
 242       *
 243       *   // Listen to only the first click on the page
 244       *   JX.Stratcom.listen('click', null, function() {
 245       *     // do interesting things
 246       *     JX.Stratcom.removeCurrentListener();
 247       *   });
 248       *
 249       * @task remove
 250       */
 251      removeCurrentListener : function() {
 252        var context = this._execContext[this._execContext.length - 1];
 253        var listeners = context.listeners;
 254        // JX.Stratcom.pass will have incremented cursor by now
 255        var cursor = context.cursor - 1;
 256        if (listeners[cursor]) {
 257          listeners[cursor].handler.remove();
 258        }
 259      },
 260  
 261  
 262      /**
 263       * Dispatch a native Javascript event through the Stratcom control flow.
 264       * Generally, this is automatically called for you by the master dispatcher
 265       * installed by ##init.js##. When you want to dispatch an application event,
 266       * you should instead call invoke().
 267       *
 268       * @param  Event       Native event for dispatch.
 269       * @return :JX.Event   Dispatched :JX.Event.
 270       * @task internal
 271       */
 272      dispatch : function(event) {
 273        var path = [];
 274        var nodes = {};
 275        var distances = {};
 276        var push = function(key, node, distance) {
 277          // we explicitly only store the first occurrence of each key
 278          if (!nodes.hasOwnProperty(key)) {
 279            nodes[key] = node;
 280            distances[key] = distance;
 281            path.push(key);
 282          }
 283        };
 284  
 285        var target = event.srcElement || event.target;
 286  
 287        // Touch events may originate from text nodes, but we want to start our
 288        // traversal from the nearest Element, so we grab the parentNode instead.
 289        if (target && target.nodeType === 3) {
 290          target = target.parentNode;
 291        }
 292  
 293        // Since you can only listen by tag, id, or sigil we unset the target if
 294        // it isn't an Element. Document and window are Nodes but not Elements.
 295        if (!target || !target.getAttribute) {
 296          target = null;
 297        }
 298  
 299        var distance = 1;
 300        var cursor = target;
 301        while (cursor && cursor.getAttribute) {
 302          push('tag:' + cursor.nodeName.toLowerCase(), cursor, distance);
 303  
 304          var id = cursor.id;
 305          if (id) {
 306            push('id:' + id, cursor, distance);
 307          }
 308  
 309          var sigils = cursor.getAttribute('data-sigil');
 310          if (sigils) {
 311            sigils = sigils.split(' ');
 312            for (var ii = 0; ii < sigils.length; ii++) {
 313              push(sigils[ii], cursor, distance);
 314            }
 315          }
 316  
 317          var auto_id = cursor.getAttribute('data-autoid');
 318          if (auto_id) {
 319            push('autoid:' + auto_id, cursor, distance);
 320          }
 321  
 322          ++distance;
 323          cursor = cursor.parentNode;
 324        }
 325  
 326        var etype = event.type;
 327        if (etype == 'focusin') {
 328          etype = 'focus';
 329        } else if (etype == 'focusout') {
 330          etype = 'blur';
 331        }
 332  
 333        var proxy = new JX.Event()
 334          .setRawEvent(event)
 335          .setData(event.customData)
 336          .setType(etype)
 337          .setTarget(target)
 338          .setNodes(nodes)
 339          .setNodeDistances(distances)
 340          .setPath(path.reverse());
 341  
 342        // Don't touch this for debugging purposes
 343        //JX.log('~> '+proxy.toString());
 344  
 345        return this._dispatchProxy(proxy);
 346      },
 347  
 348  
 349      /**
 350       * Dispatch a previously constructed proxy :JX.Event.
 351       *
 352       * @param  :JX.Event Event to dispatch.
 353       * @return :JX.Event Returns the event argument.
 354       * @task internal
 355       */
 356      _dispatchProxy : function(proxy) {
 357  
 358        var scope = this._targets[proxy.getType()];
 359  
 360        if (!scope) {
 361          return proxy;
 362        }
 363  
 364        var path = proxy.getPath();
 365        var distances = proxy.getNodeDistances();
 366        var len = path.length;
 367        var hits = {};
 368        var hit_distances = {};
 369        var matches;
 370  
 371        // A large number (larger than any distance we will ever encounter), but
 372        // we need to do math on it in the sort function so we can't use
 373        // Number.POSITIVE_INFINITY.
 374        var far_away = 1000000;
 375  
 376        for (var root = -1; root < len; ++root) {
 377          matches = scope[(root == -1) ? this._auto : path[root]];
 378          if (matches) {
 379            var distance = distances[path[root]] || far_away;
 380            for (var ii = 0; ii < matches.length; ++ii) {
 381              var match = matches[ii];
 382              hits[match] = (hits[match] || 0) + 1;
 383              hit_distances[match] = Math.min(
 384                hit_distances[match] || distance,
 385                distance
 386              );
 387            }
 388          }
 389        }
 390  
 391        var listeners = [];
 392  
 393        for (var k in hits) {
 394          if (hits[k] == this._need[k]) {
 395            var handler = this._handlers[k];
 396            if (handler) {
 397              listeners.push({
 398                distance: hit_distances[k],
 399                handler: handler
 400              });
 401            }
 402          }
 403        }
 404  
 405        // Sort listeners by matched sigil closest to the target node
 406        // Listeners with the same closest sigil are called in an undefined order
 407        listeners.sort(function(a, b) {
 408          if (__DEV__) {
 409            // Make sure people play by the rules. >:)
 410            return (a.distance - b.distance) || (Math.random() - 0.5);
 411          }
 412          return a.distance - b.distance;
 413        });
 414  
 415        this._execContext.push({
 416          listeners: listeners,
 417          event: proxy,
 418          cursor: 0
 419        });
 420  
 421        this.pass();
 422  
 423        this._execContext.pop();
 424  
 425        return proxy;
 426      },
 427  
 428  
 429      /**
 430       * Pass on an event, allowing other handlers to process it. The use case
 431       * here is generally something like:
 432       *
 433       *   if (JX.Stratcom.pass()) {
 434       *     // something else handled the event
 435       *     return;
 436       *   }
 437       *   // handle the event
 438       *   event.prevent();
 439       *
 440       * This allows you to install event handlers that operate at a lower
 441       * effective priority, and provide a default behavior which is overridable
 442       * by listeners.
 443       *
 444       * @return bool  True if the event was stopped or prevented by another
 445       *               handler.
 446       * @task handle
 447       */
 448      pass : function() {
 449        var context = this._execContext[this._execContext.length - 1];
 450        var event = context.event;
 451        var listeners = context.listeners;
 452        while (context.cursor < listeners.length) {
 453          var cursor = context.cursor++;
 454          if (listeners[cursor]) {
 455            var handler = listeners[cursor].handler;
 456            handler._callback && handler._callback(event);
 457          }
 458          if (event.getStopped()) {
 459            break;
 460          }
 461        }
 462        return event.getStopped() || event.getPrevented();
 463      },
 464  
 465  
 466      /**
 467       * Retrieve the event (if any) which is currently being dispatched.
 468       *
 469       * @return :JX.Event|null   Event which is currently being dispatched, or
 470       *                          null if there is no active dispatch.
 471       * @task handle
 472       */
 473      context : function() {
 474        var len = this._execContext.length;
 475        return len ? this._execContext[len - 1].event : null;
 476      },
 477  
 478  
 479      /**
 480       * Merge metadata. You must call this (even if you have no metadata) to
 481       * start the Stratcom queue.
 482       *
 483       * @param  int          The datablock to merge data into.
 484       * @param  dict         Dictionary of metadata.
 485       * @return void
 486       * @task internal
 487       */
 488      mergeData : function(block, data) {
 489        if (this._data[block]) {
 490          if (__DEV__) {
 491            for (var key in data) {
 492              if (key in this._data[block]) {
 493                JX.$E(
 494                  'JX.Stratcom.mergeData(' + block + ', ...); is overwriting ' +
 495                  'existing data.');
 496              }
 497            }
 498          }
 499          JX.copy(this._data[block], data);
 500        } else {
 501          this._data[block] = data;
 502          if (block === 0) {
 503            JX.Stratcom.ready = true;
 504            JX.flushHoldingQueue('install-init', function(fn) {
 505              fn();
 506            });
 507            JX.__rawEventQueue({type: 'start-queue'});
 508          }
 509        }
 510      },
 511  
 512  
 513      /**
 514       * Determine if a node has a specific sigil.
 515       *
 516       * @param  Node    Node to test.
 517       * @param  string  Sigil to check for.
 518       * @return bool    True if the node has the sigil.
 519       *
 520       * @task sigil
 521       */
 522      hasSigil : function(node, sigil) {
 523        if (__DEV__) {
 524          if (!node || !node.getAttribute) {
 525            JX.$E(
 526              'JX.Stratcom.hasSigil(<non-element>, ...): ' +
 527              'node is not an element. Most likely, you\'re passing window or ' +
 528              'document, which are not elements and can\'t have sigils.');
 529          }
 530        }
 531  
 532        var sigils = node.getAttribute('data-sigil') || false;
 533        return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1;
 534      },
 535  
 536  
 537      /**
 538       * Add a sigil to a node.
 539       *
 540       * @param   Node    Node to add the sigil to.
 541       * @param   string  Sigil to name the node with.
 542       * @return  void
 543       * @task sigil
 544       */
 545      addSigil: function(node, sigil) {
 546        if (__DEV__) {
 547          if (!node || !node.getAttribute) {
 548            JX.$E(
 549              'JX.Stratcom.addSigil(<non-element>, ...): ' +
 550              'node is not an element. Most likely, you\'re passing window or ' +
 551              'document, which are not elements and can\'t have sigils.');
 552          }
 553        }
 554  
 555        var sigils = node.getAttribute('data-sigil') || '';
 556        if (!JX.Stratcom.hasSigil(node, sigil)) {
 557          sigils += ' ' + sigil;
 558        }
 559  
 560        node.setAttribute('data-sigil', sigils);
 561      },
 562  
 563  
 564      /**
 565       * Retrieve a node's metadata.
 566       *
 567       * @param   Node    Node from which to retrieve data.
 568       * @return  object  Data attached to the node. If no data has been attached
 569       *                  to the node yet, an empty object will be returned, but
 570       *                  subsequent calls to this method will always retrieve the
 571       *                  same object.
 572       * @task meta
 573       */
 574      getData : function(node) {
 575        if (__DEV__) {
 576          if (!node || !node.getAttribute) {
 577            JX.$E(
 578              'JX.Stratcom.getData(<non-element>): ' +
 579              'node is not an element. Most likely, you\'re passing window or ' +
 580              'document, which are not elements and can\'t have data.');
 581          }
 582        }
 583  
 584        var meta_id = (node.getAttribute('data-meta') || '').split('_');
 585        if (meta_id[0] && meta_id[1]) {
 586          var block = this._data[meta_id[0]];
 587          var index = meta_id[1];
 588          if (block && (index in block)) {
 589            return block[index];
 590          } else if (__DEV__) {
 591            JX.$E(
 592              'JX.Stratcom.getData(<node>): Tried to access data (block ' +
 593              meta_id[0] + ', index ' + index + ') that was not present. This ' +
 594              'probably means you are calling getData() before the block ' +
 595              'is provided by mergeData().');
 596          }
 597        }
 598  
 599        var data = {};
 600        if (!this._data[1]) { // data block 1 is reserved for JavaScript
 601          this._data[1] = {};
 602        }
 603        this._data[1][this._dataIndex] = data;
 604        node.setAttribute('data-meta', '1_' + (this._dataIndex++));
 605        return data;
 606      },
 607  
 608  
 609      /**
 610       * Add data to a node's metadata.
 611       *
 612       * @param   Node    Node which data should be attached to.
 613       * @param   object  Data to add to the node's metadata.
 614       * @return  object  Data attached to the node that is returned by
 615       *                  JX.Stratcom.getData().
 616       * @task meta
 617       */
 618      addData : function(node, data) {
 619        if (__DEV__) {
 620          if (!node || !node.getAttribute) {
 621            JX.$E(
 622              'JX.Stratcom.addData(<non-element>, ...): ' +
 623              'node is not an element. Most likely, you\'re passing window or ' +
 624              'document, which are not elements and can\'t have sigils.');
 625          }
 626          if (!data || typeof data != 'object') {
 627            JX.$E(
 628              'JX.Stratcom.addData(..., <nonobject>): ' +
 629              'data to attach to node is not an object. You must use ' +
 630              'objects, not primitives, for metadata.');
 631          }
 632        }
 633  
 634        return JX.copy(JX.Stratcom.getData(node), data);
 635      },
 636  
 637  
 638      /**
 639       * @task internal
 640       */
 641      allocateMetadataBlock : function() {
 642        return this._dataBlock++;
 643      }
 644    }
 645  });


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