[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/yuilib/3.17.2/transition/ -> transition.js (source)

   1  /*
   2  YUI 3.17.2 (build 9c3c78e)
   3  Copyright 2014 Yahoo! Inc. All rights reserved.
   4  Licensed under the BSD License.
   5  http://yuilibrary.com/license/
   6  */
   7  
   8  YUI.add('transition', function (Y, NAME) {
   9  
  10  /**
  11  * Provides the transition method for Node.
  12  * Transition has no API of its own, but adds the transition method to Node.
  13  *
  14  * @module transition
  15  * @requires node-style
  16  */
  17  
  18  var CAMEL_VENDOR_PREFIX = '',
  19      VENDOR_PREFIX = '',
  20      DOCUMENT = Y.config.doc,
  21      DOCUMENT_ELEMENT = 'documentElement',
  22      DOCUMENT_STYLE = DOCUMENT[DOCUMENT_ELEMENT].style,
  23      TRANSITION_CAMEL = 'transition',
  24      TRANSITION_PROPERTY_CAMEL = 'transitionProperty',
  25      TRANSITION_PROPERTY,
  26      TRANSITION_DURATION,
  27      TRANSITION_TIMING_FUNCTION,
  28      TRANSITION_DELAY,
  29      TRANSITION_END,
  30      ON_TRANSITION_END,
  31  
  32      EMPTY_OBJ = {},
  33  
  34      VENDORS = [
  35          'Webkit',
  36          'Moz'
  37      ],
  38  
  39      VENDOR_TRANSITION_END = {
  40          Webkit: 'webkitTransitionEnd'
  41      },
  42  
  43  /**
  44   * A class for constructing transition instances.
  45   * Adds the "transition" method to Node.
  46   * @class Transition
  47   * @constructor
  48   */
  49  
  50  Transition = function() {
  51      this.init.apply(this, arguments);
  52  };
  53  
  54  // One off handling of transform-prefixing.
  55  Transition._TRANSFORM = 'transform';
  56  
  57  Transition._toCamel = function(property) {
  58      property = property.replace(/-([a-z])/gi, function(m0, m1) {
  59          return m1.toUpperCase();
  60      });
  61  
  62      return property;
  63  };
  64  
  65  Transition._toHyphen = function(property) {
  66      property = property.replace(/([A-Z]?)([a-z]+)([A-Z]?)/g, function(m0, m1, m2, m3) {
  67          var str = ((m1) ? '-' + m1.toLowerCase() : '') + m2;
  68  
  69          if (m3) {
  70              str += '-' + m3.toLowerCase();
  71          }
  72  
  73          return str;
  74      });
  75  
  76      return property;
  77  };
  78  
  79  Transition.SHOW_TRANSITION = 'fadeIn';
  80  Transition.HIDE_TRANSITION = 'fadeOut';
  81  
  82  Transition.useNative = false;
  83  
  84  // Map transition properties to vendor-specific versions.
  85  if ('transition' in DOCUMENT_STYLE
  86      && 'transitionProperty' in DOCUMENT_STYLE
  87      && 'transitionDuration' in DOCUMENT_STYLE
  88      && 'transitionTimingFunction' in DOCUMENT_STYLE
  89      && 'transitionDelay' in DOCUMENT_STYLE) {
  90      Transition.useNative = true;
  91      Transition.supported = true; // TODO: remove
  92  } else {
  93      Y.Array.each(VENDORS, function(val) { // then vendor specific
  94          var property = val + 'Transition';
  95          if (property in DOCUMENT[DOCUMENT_ELEMENT].style) {
  96              CAMEL_VENDOR_PREFIX = val;
  97              VENDOR_PREFIX       = Transition._toHyphen(val) + '-';
  98  
  99              Transition.useNative = true;
 100              Transition.supported = true; // TODO: remove
 101              Transition._VENDOR_PREFIX = val;
 102          }
 103      });
 104  }
 105  
 106  // Map transform property to vendor-specific versions.
 107  // One-off required for cssText injection.
 108  if (typeof DOCUMENT_STYLE.transform === 'undefined') {
 109      Y.Array.each(VENDORS, function(val) { // then vendor specific
 110          var property = val + 'Transform';
 111          if (typeof DOCUMENT_STYLE[property] !== 'undefined') {
 112              Transition._TRANSFORM = property;
 113          }
 114      });
 115  }
 116  
 117  if (CAMEL_VENDOR_PREFIX) {
 118      TRANSITION_CAMEL          = CAMEL_VENDOR_PREFIX + 'Transition';
 119      TRANSITION_PROPERTY_CAMEL = CAMEL_VENDOR_PREFIX + 'TransitionProperty';
 120  }
 121  
 122  TRANSITION_PROPERTY        = VENDOR_PREFIX + 'transition-property';
 123  TRANSITION_DURATION        = VENDOR_PREFIX + 'transition-duration';
 124  TRANSITION_TIMING_FUNCTION = VENDOR_PREFIX + 'transition-timing-function';
 125  TRANSITION_DELAY           = VENDOR_PREFIX + 'transition-delay';
 126  
 127  TRANSITION_END    = 'transitionend';
 128  ON_TRANSITION_END = 'on' + CAMEL_VENDOR_PREFIX.toLowerCase() + 'transitionend';
 129  TRANSITION_END    = VENDOR_TRANSITION_END[CAMEL_VENDOR_PREFIX] || TRANSITION_END;
 130  
 131  Transition.fx = {};
 132  Transition.toggles = {};
 133  
 134  Transition._hasEnd = {};
 135  
 136  Transition._reKeywords = /^(?:node|duration|iterations|easing|delay|on|onstart|onend)$/i;
 137  
 138  Y.Node.DOM_EVENTS[TRANSITION_END] = 1;
 139  
 140  Transition.NAME = 'transition';
 141  
 142  Transition.DEFAULT_EASING = 'ease';
 143  Transition.DEFAULT_DURATION = 0.5;
 144  Transition.DEFAULT_DELAY = 0;
 145  
 146  Transition._nodeAttrs = {};
 147  
 148  Transition.prototype = {
 149      constructor: Transition,
 150      init: function(node, config) {
 151          var anim = this;
 152          anim._node = node;
 153          if (!anim._running && config) {
 154              anim._config = config;
 155              node._transition = anim; // cache for reuse
 156  
 157              anim._duration = ('duration' in config) ?
 158                  config.duration: anim.constructor.DEFAULT_DURATION;
 159  
 160              anim._delay = ('delay' in config) ?
 161                  config.delay: anim.constructor.DEFAULT_DELAY;
 162  
 163              anim._easing = config.easing || anim.constructor.DEFAULT_EASING;
 164              anim._count = 0; // track number of animated properties
 165              anim._running = false;
 166  
 167          }
 168  
 169          return anim;
 170      },
 171  
 172      addProperty: function(prop, config) {
 173          var anim = this,
 174              node = this._node,
 175              uid = Y.stamp(node),
 176              nodeInstance = Y.one(node),
 177              attrs = Transition._nodeAttrs[uid],
 178              computed,
 179              compareVal,
 180              dur,
 181              attr,
 182              val;
 183  
 184          if (!attrs) {
 185              attrs = Transition._nodeAttrs[uid] = {};
 186          }
 187  
 188          attr = attrs[prop];
 189  
 190          // might just be a value
 191          if (config && config.value !== undefined) {
 192              val = config.value;
 193          } else if (config !== undefined) {
 194              val = config;
 195              config = EMPTY_OBJ;
 196          }
 197  
 198          if (typeof val === 'function') {
 199              val = val.call(nodeInstance, nodeInstance);
 200          }
 201  
 202          if (attr && attr.transition) {
 203              // take control if another transition owns this property
 204              if (attr.transition !== anim) {
 205                  attr.transition._count--; // remapping attr to this transition
 206              }
 207          }
 208  
 209          anim._count++; // properties per transition
 210  
 211          // make 0 async and fire events
 212          dur = ((typeof config.duration !== 'undefined') ? config.duration :
 213                      anim._duration) || 0.0001;
 214  
 215          attrs[prop] = {
 216              value: val,
 217              duration: dur,
 218              delay: (typeof config.delay !== 'undefined') ? config.delay :
 219                      anim._delay,
 220  
 221              easing: config.easing || anim._easing,
 222  
 223              transition: anim
 224          };
 225  
 226          // native end event doesnt fire when setting to same value
 227          // supplementing with timer
 228          // val may be a string or number (height: 0, etc), but computedStyle is always string
 229          computed = Y.DOM.getComputedStyle(node, prop);
 230          compareVal = (typeof val === 'string') ? computed : parseFloat(computed);
 231  
 232          if (Transition.useNative && compareVal === val) {
 233              setTimeout(function() {
 234                  anim._onNativeEnd.call(node, {
 235                      propertyName: prop,
 236                      elapsedTime: dur
 237                  });
 238              }, dur * 1000);
 239          }
 240      },
 241  
 242      removeProperty: function(prop) {
 243          var anim = this,
 244              attrs = Transition._nodeAttrs[Y.stamp(anim._node)];
 245  
 246          if (attrs && attrs[prop]) {
 247              delete attrs[prop];
 248              anim._count--;
 249          }
 250  
 251      },
 252  
 253      initAttrs: function(config) {
 254          var attr,
 255              node = this._node;
 256  
 257          if (config.transform && !config[Transition._TRANSFORM]) {
 258              config[Transition._TRANSFORM] = config.transform;
 259              delete config.transform; // TODO: copy
 260          }
 261  
 262          for (attr in config) {
 263              if (config.hasOwnProperty(attr) && !Transition._reKeywords.test(attr)) {
 264                  this.addProperty(attr, config[attr]);
 265  
 266                  // when size is auto or % webkit starts from zero instead of computed
 267                  // (https://bugs.webkit.org/show_bug.cgi?id=16020)
 268                  // TODO: selective set
 269                  if (node.style[attr] === '') {
 270                      Y.DOM.setStyle(node, attr, Y.DOM.getComputedStyle(node, attr));
 271                  }
 272              }
 273          }
 274      },
 275  
 276      /**
 277       * Starts or an animation.
 278       * @method run
 279       * @chainable
 280       * @private
 281       */
 282      run: function(callback) {
 283          var anim = this,
 284              node = anim._node,
 285              config = anim._config,
 286              data = {
 287                  type: 'transition:start',
 288                  config: config
 289              };
 290  
 291  
 292          if (!anim._running) {
 293              anim._running = true;
 294  
 295              if (config.on && config.on.start) {
 296                  config.on.start.call(Y.one(node), data);
 297              }
 298  
 299              anim.initAttrs(anim._config);
 300  
 301              anim._callback = callback;
 302              anim._start();
 303          }
 304  
 305  
 306          return anim;
 307      },
 308  
 309      _start: function() {
 310          this._runNative();
 311      },
 312  
 313      _prepDur: function(dur) {
 314          dur = parseFloat(dur) * 1000;
 315  
 316          return dur + 'ms';
 317      },
 318  
 319      _runNative: function() {
 320          var anim = this,
 321              node = anim._node,
 322              uid = Y.stamp(node),
 323              style = node.style,
 324              computed = node.ownerDocument.defaultView.getComputedStyle(node),
 325              attrs = Transition._nodeAttrs[uid],
 326              cssText = '',
 327              cssTransition = computed[Transition._toCamel(TRANSITION_PROPERTY)],
 328  
 329              transitionText = TRANSITION_PROPERTY + ': ',
 330              duration = TRANSITION_DURATION + ': ',
 331              easing = TRANSITION_TIMING_FUNCTION + ': ',
 332              delay = TRANSITION_DELAY + ': ',
 333              hyphy,
 334              attr,
 335              name;
 336  
 337          // preserve existing transitions
 338          if (cssTransition !== 'all') {
 339              transitionText += cssTransition + ',';
 340              duration += computed[Transition._toCamel(TRANSITION_DURATION)] + ',';
 341              easing += computed[Transition._toCamel(TRANSITION_TIMING_FUNCTION)] + ',';
 342              delay += computed[Transition._toCamel(TRANSITION_DELAY)] + ',';
 343  
 344          }
 345  
 346          // run transitions mapped to this instance
 347          for (name in attrs) {
 348              hyphy = Transition._toHyphen(name);
 349              attr = attrs[name];
 350              if ((attr = attrs[name]) && attr.transition === anim) {
 351                  if (name in node.style) { // only native styles allowed
 352                      duration += anim._prepDur(attr.duration) + ',';
 353                      delay += anim._prepDur(attr.delay) + ',';
 354                      easing += (attr.easing) + ',';
 355  
 356                      transitionText += hyphy + ',';
 357                      cssText += hyphy + ': ' + attr.value + '; ';
 358                  } else {
 359                      this.removeProperty(name);
 360                  }
 361              }
 362          }
 363  
 364          transitionText = transitionText.replace(/,$/, ';');
 365          duration = duration.replace(/,$/, ';');
 366          easing = easing.replace(/,$/, ';');
 367          delay = delay.replace(/,$/, ';');
 368  
 369          // only one native end event per node
 370          if (!Transition._hasEnd[uid]) {
 371              node.addEventListener(TRANSITION_END, anim._onNativeEnd, '');
 372              Transition._hasEnd[uid] = true;
 373  
 374          }
 375  
 376          style.cssText += transitionText + duration + easing + delay + cssText;
 377  
 378      },
 379  
 380      _end: function(elapsed) {
 381          var anim = this,
 382              node = anim._node,
 383              callback = anim._callback,
 384              config = anim._config,
 385              data = {
 386                  type: 'transition:end',
 387                  config: config,
 388                  elapsedTime: elapsed
 389              },
 390  
 391              nodeInstance = Y.one(node);
 392  
 393          anim._running = false;
 394          anim._callback = null;
 395  
 396          if (node) {
 397              if (config.on && config.on.end) {
 398                  setTimeout(function() { // IE: allow previous update to finish
 399                      config.on.end.call(nodeInstance, data);
 400  
 401                      // nested to ensure proper fire order
 402                      if (callback) {
 403                          callback.call(nodeInstance, data);
 404                      }
 405  
 406                  }, 1);
 407              } else if (callback) {
 408                  setTimeout(function() { // IE: allow previous update to finish
 409                      callback.call(nodeInstance, data);
 410                  }, 1);
 411              }
 412          }
 413  
 414      },
 415  
 416      _endNative: function(name) {
 417          var node = this._node,
 418              value = node.ownerDocument.defaultView.getComputedStyle(node, '')[Transition._toCamel(TRANSITION_PROPERTY)];
 419  
 420          name = Transition._toHyphen(name);
 421          if (typeof value === 'string') {
 422              value = value.replace(new RegExp('(?:^|,\\s)' + name + ',?'), ',');
 423              value = value.replace(/^,|,$/, '');
 424              node.style[TRANSITION_CAMEL] = value;
 425          }
 426      },
 427  
 428      _onNativeEnd: function(e) {
 429          var node = this,
 430              uid = Y.stamp(node),
 431              event = e,//e._event,
 432              name = Transition._toCamel(event.propertyName),
 433              elapsed = event.elapsedTime,
 434              attrs = Transition._nodeAttrs[uid],
 435              attr = attrs[name],
 436              anim = (attr) ? attr.transition : null,
 437              data,
 438              config;
 439  
 440          if (anim) {
 441              anim.removeProperty(name);
 442              anim._endNative(name);
 443              config = anim._config[name];
 444  
 445              data = {
 446                  type: 'propertyEnd',
 447                  propertyName: name,
 448                  elapsedTime: elapsed,
 449                  config: config
 450              };
 451  
 452              if (config && config.on && config.on.end) {
 453                  config.on.end.call(Y.one(node), data);
 454              }
 455  
 456              if (anim._count <= 0)  { // after propertyEnd fires
 457                  anim._end(elapsed);
 458                  node.style[TRANSITION_PROPERTY_CAMEL] = ''; // clean up style
 459              }
 460          }
 461      },
 462  
 463      destroy: function() {
 464          var anim = this,
 465              node = anim._node;
 466  
 467          if (node) {
 468              node.removeEventListener(TRANSITION_END, anim._onNativeEnd, false);
 469              anim._node = null;
 470          }
 471      }
 472  };
 473  
 474  Y.Transition = Transition;
 475  Y.TransitionNative = Transition; // TODO: remove
 476  
 477  /**
 478   *   Animate one or more css properties to a given value. Requires the "transition" module.
 479   *   <pre>example usage:
 480   *       Y.one('#demo').transition({
 481   *           duration: 1, // in seconds, default is 0.5
 482   *           easing: 'ease-out', // default is 'ease'
 483   *           delay: '1', // delay start for 1 second, default is 0
 484   *
 485   *           height: '10px',
 486   *           width: '10px',
 487   *
 488   *           opacity: { // per property
 489   *               value: 0,
 490   *               duration: 2,
 491   *               delay: 2,
 492   *               easing: 'ease-in'
 493   *           }
 494   *       });
 495   *   </pre>
 496   *   @for Node
 497   *   @method transition
 498   *   @param {Object} config An object containing one or more style properties, a duration and an easing.
 499   *   @param {Function} callback A function to run after the transition has completed.
 500   *   @chainable
 501  */
 502  Y.Node.prototype.transition = function(name, config, callback) {
 503      var
 504          transitionAttrs = Transition._nodeAttrs[Y.stamp(this._node)],
 505          anim = (transitionAttrs) ? transitionAttrs.transition || null : null,
 506          fxConfig,
 507          prop;
 508  
 509      if (typeof name === 'string') { // named effect, pull config from registry
 510          if (typeof config === 'function') {
 511              callback = config;
 512              config = null;
 513          }
 514  
 515          fxConfig = Transition.fx[name];
 516  
 517          if (config && typeof config === 'object') {
 518              config = Y.clone(config);
 519  
 520              for (prop in fxConfig) {
 521                  if (fxConfig.hasOwnProperty(prop)) {
 522                      if (! (prop in config)) {
 523                          config[prop] = fxConfig[prop];
 524                      }
 525                  }
 526              }
 527          } else {
 528              config = fxConfig;
 529          }
 530  
 531      } else { // name is a config, config is a callback or undefined
 532          callback = config;
 533          config = name;
 534      }
 535  
 536      if (anim && !anim._running) {
 537          anim.init(this, config);
 538      } else {
 539          anim = new Transition(this._node, config);
 540      }
 541  
 542      anim.run(callback);
 543      return this;
 544  };
 545  
 546  Y.Node.prototype.show = function(name, config, callback) {
 547      this._show(); // show prior to transition
 548      if (name && Y.Transition) {
 549          if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
 550              if (typeof config === 'function') {
 551                  callback = config;
 552                  config = name;
 553              }
 554              name = Transition.SHOW_TRANSITION;
 555          }
 556          this.transition(name, config, callback);
 557      }
 558      return this;
 559  };
 560  
 561  Y.NodeList.prototype.show = function(name, config, callback) {
 562      var nodes = this._nodes,
 563          i = 0,
 564          node;
 565  
 566      while ((node = nodes[i++])) {
 567          Y.one(node).show(name, config, callback);
 568      }
 569  
 570      return this;
 571  };
 572  
 573  
 574  
 575  var _wrapCallBack = function(anim, fn, callback) {
 576      return function() {
 577          if (fn) {
 578              fn.call(anim);
 579          }
 580          if (callback && typeof callback === 'function') {
 581              callback.apply(anim._node, arguments);
 582          }
 583      };
 584  };
 585  
 586  Y.Node.prototype.hide = function(name, config, callback) {
 587      if (name && Y.Transition) {
 588          if (typeof config === 'function') {
 589              callback = config;
 590              config = null;
 591          }
 592  
 593          callback = _wrapCallBack(this, this._hide, callback); // wrap with existing callback
 594          if (typeof name !== 'string' && !name.push) { // named effect or array of effects supercedes default
 595              if (typeof config === 'function') {
 596                  callback = config;
 597                  config = name;
 598              }
 599              name = Transition.HIDE_TRANSITION;
 600          }
 601          this.transition(name, config, callback);
 602      } else {
 603          this._hide();
 604      }
 605      return this;
 606  };
 607  
 608  Y.NodeList.prototype.hide = function(name, config, callback) {
 609      var nodes = this._nodes,
 610          i = 0,
 611          node;
 612  
 613      while ((node = nodes[i++])) {
 614          Y.one(node).hide(name, config, callback);
 615      }
 616  
 617      return this;
 618  };
 619  
 620  /**
 621   *   Animate one or more css properties to a given value. Requires the "transition" module.
 622   *   <pre>example usage:
 623   *       Y.all('.demo').transition({
 624   *           duration: 1, // in seconds, default is 0.5
 625   *           easing: 'ease-out', // default is 'ease'
 626   *           delay: '1', // delay start for 1 second, default is 0
 627   *
 628   *           height: '10px',
 629   *           width: '10px',
 630   *
 631   *           opacity: { // per property
 632   *               value: 0,
 633   *               duration: 2,
 634   *               delay: 2,
 635   *               easing: 'ease-in'
 636   *           }
 637   *       });
 638   *   </pre>
 639   *   @for NodeList
 640   *   @method transition
 641   *   @param {Object} config An object containing one or more style properties, a duration and an easing.
 642   *   @param {Function} callback A function to run after the transition has completed. The callback fires
 643   *       once per item in the NodeList.
 644   *   @param {Boolean} callbackOnce If true, the callback will be called only after the
 645   *       last transition has completed
 646   *   @chainable
 647  */
 648  Y.NodeList.prototype.transition = function(config, callback, callbackOnce) {
 649      var nodes = this._nodes,
 650          size = this.size(),
 651           i = 0,
 652          callbackOnce = callbackOnce === true,
 653          node;
 654  
 655      while ((node = nodes[i++])) {
 656          if (i < size && callbackOnce){
 657              Y.one(node).transition(config);
 658          } else {
 659              Y.one(node).transition(config, callback);
 660          }
 661      }
 662  
 663      return this;
 664  };
 665  
 666  Y.Node.prototype.toggleView = function(name, on, callback) {
 667      this._toggles = this._toggles || [];
 668      callback = arguments[arguments.length - 1];
 669  
 670      if (typeof name !== 'string') { // no transition, just toggle
 671          on = name;
 672          this._toggleView(on, callback); // call original _toggleView in Y.Node
 673          return;
 674      }
 675  
 676      if (typeof on === 'function') { // Ignore "on" if used for callback argument.
 677          on = undefined;
 678      }
 679  
 680      if (typeof on === 'undefined' && name in this._toggles) { // reverse current toggle
 681          on = ! this._toggles[name];
 682      }
 683  
 684      on = (on) ? 1 : 0;
 685      if (on) {
 686          this._show();
 687      }  else {
 688          callback = _wrapCallBack(this, this._hide, callback);
 689      }
 690  
 691      this._toggles[name] = on;
 692      this.transition(Y.Transition.toggles[name][on], callback);
 693  
 694      return this;
 695  };
 696  
 697  Y.NodeList.prototype.toggleView = function(name, on, callback) {
 698      var nodes = this._nodes,
 699          i = 0,
 700          node;
 701  
 702      while ((node = nodes[i++])) {
 703          node = Y.one(node);
 704          node.toggleView.apply(node, arguments);
 705      }
 706  
 707      return this;
 708  };
 709  
 710  Y.mix(Transition.fx, {
 711      fadeOut: {
 712          opacity: 0,
 713          duration: 0.5,
 714          easing: 'ease-out'
 715      },
 716  
 717      fadeIn: {
 718          opacity: 1,
 719          duration: 0.5,
 720          easing: 'ease-in'
 721      },
 722  
 723      sizeOut: {
 724          height: 0,
 725          width: 0,
 726          duration: 0.75,
 727          easing: 'ease-out'
 728      },
 729  
 730      sizeIn: {
 731          height: function(node) {
 732              return node.get('scrollHeight') + 'px';
 733          },
 734          width: function(node) {
 735              return node.get('scrollWidth') + 'px';
 736          },
 737          duration: 0.5,
 738          easing: 'ease-in',
 739  
 740          on: {
 741              start: function() {
 742                  var overflow = this.getStyle('overflow');
 743                  if (overflow !== 'hidden') { // enable scrollHeight/Width
 744                      this.setStyle('overflow', 'hidden');
 745                      this._transitionOverflow = overflow;
 746                  }
 747              },
 748  
 749              end: function() {
 750                  if (this._transitionOverflow) { // revert overridden value
 751                      this.setStyle('overflow', this._transitionOverflow);
 752                      delete this._transitionOverflow;
 753                  }
 754              }
 755          }
 756      }
 757  });
 758  
 759  Y.mix(Transition.toggles, {
 760      size: ['sizeOut', 'sizeIn'],
 761      fade: ['fadeOut', 'fadeIn']
 762  });
 763  
 764  
 765  }, '3.17.2', {"requires": ["node-style"]});


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1