<commit_before>/*!
	Wheelzoom 1.0.0
	license: MIT
	https://github.com/Luperi/wheelzoom
*/

// create CustomEvent to override window.Event in unsupported browsers
(function () {
	function CustomEvent ( event, params ) {
		params = params || { bubbles: false, cancelable: false, detail: undefined };
		var evt = document.createEvent( 'CustomEvent' );
		evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
		return evt;
	}

	CustomEvent.prototype = window.Event.prototype;
	window.CustomEvent = CustomEvent;
})();

// create cross browser triggerEvent function
window.triggerEvent = function(target, eventName, params) {
	if (params) {
		var e = new CustomEvent(eventName, {
			'detail': params
		});
	} else {
		try {
			var e = new Event(eventName);
		} catch (err) {
			var e = new CustomEvent(eventName);
		}
	}

	return target.dispatchEvent(e, params);
};

window.wheelzoom = (function(){
	var defaults = {
		zoom: 0.10,
		maxZoom: -1
	};

	var canvas = document.createElement('canvas');

	var main = function(img, options){
		if (!img || !img.nodeName || img.nodeName !== 'IMG') { return; }

		var settings = {};
		var width;
		var height;
		var bgWidth;
		var bgHeight;
		var bgPosX;
		var bgPosY;
		var previousEvent;
		var cachedDataUrl;

		function setSrcToBackground(img) {
			img.style.backgroundImage = 'url("'+img.src+'")';
			img.style.backgroundRepeat = 'no-repeat';
			canvas.width = img.naturalWidth;
			canvas.height = img.naturalHeight;
			cachedDataUrl = canvas.toDataURL();
			img.src = cachedDataUrl;
		}

		function updateBgStyle() {
			if (bgPosX > 0) {
				bgPosX = 0;
			} else if (bgPosX < width - bgWidth) {
				bgPosX = width - bgWidth;
			}

			if (bgPosY > 0) {
				bgPosY = 0;
			} else if (bgPosY < height - bgHeight) {
				bgPosY = height - bgHeight;
			}

			img.style.backgroundSize = bgWidth+'px '+bgHeight+'px';
			img.style.backgroundPosition = bgPosX+'px '+bgPosY+'px';
		}

		function reset() {
			bgWidth = width;
			bgHeight = height;
			bgPosX = bgPosY = 0;
			updateBgStyle();
		}

		img.doZoom = function (deltaY, offsetX, offsetY, propagate) {
			if (typeof propagate === 'undefined') {
				propagate = true;
			}

			// Record the offset between the bg edge and cursor:
			var bgCursorX = offsetX - bgPosX;
			var bgCursorY = offsetY - bgPosY;
			// Use the previous offset to get the percent offset between the bg edge and cursor:
			var bgRatioX = bgCursorX/bgWidth;
			var bgRatioY = bgCursorY/bgHeight;

			// Update the bg size:
			if (deltaY < 0) {
				if (settings.maxZoom == -1 || (bgWidth + bgWidth*settings.zoom) / width <= settings.maxZoom) {
					bgWidth += bgWidth*settings.zoom;
					bgHeight += bgHeight*settings.zoom;
				}
			} else {
				bgWidth -= bgWidth*settings.zoom;
				bgHeight -= bgHeight*settings.zoom;
			}

			// Take the percent offset and apply it to the new size:
			bgPosX = offsetX - (bgWidth * bgRatioX);
			bgPosY = offsetY - (bgHeight * bgRatioY);

			if (propagate) {
				if (deltaY < 0) {
					// setTimeout to handle lot of events fired
					setTimeout(function() {
						triggerEvent(img, 'wheelzoom.in', {
							bgWidth: bgWidth,
							bgHeight: bgHeight,
							bgPosX: bgPosX,
							bgPosY: bgPosY,
							width: width,
							height: height,
							bgCursorX: bgCursorX,
							bgCursorY: bgCursorY
						});
					}, 10);
				} else {
					// setTimeout to handle lot of events fired
					setTimeout(function() {
						triggerEvent(img, 'wheelzoom.out', {
							bgWidth: bgWidth,
							bgHeight: bgHeight,
							bgPosX: bgPosX,
							bgPosY: bgPosY,
							width: width,
							height: height,
							bgCursorX: bgCursorX,
							bgCursorY: bgCursorY
						});
					}, 10);
				}
			}

			// Prevent zooming out beyond the starting size
			if (bgWidth <= width || bgHeight <= height) {
				triggerEvent(img, 'wheelzoom.reset');
			} else {
				updateBgStyle();
			}
		}

		function onwheel(e) {
			var deltaY = 0;

			e.preventDefault();

			if (e.deltaY) { // FireFox 17+ (IE9+, Chrome 31+?)
				deltaY = e.deltaY;
			} else if (e.wheelDelta) {
				deltaY = -e.wheelDelta;
			}

			// As far as I know, there is no good cross-browser way to get the cursor position relative to the event target.
			// We have to calculate the target element's position relative to the document, and subtrack that from the
			// cursor's position relative to the document.
			var rect = img.getBoundingClientRect();
			var offsetX = e.pageX - rect.left - window.pageXOffset;
			var offsetY = e.pageY - rect.top - window.pageYOffset;

			img.doZoom(deltaY, offsetX, offsetY);
		}

		img.doDrag = function (xShift, yShift) {
			bgPosX += xShift;
			bgPosY += yShift;

			updateBgStyle();
		}

		function drag(e) {
			e.preventDefault();
			var xShift = e.pageX - previousEvent.pageX;
			var yShift = e.pageY - previousEvent.pageY;

			img.doDrag(xShift, yShift);

			triggerEvent(img, 'wheelzoom.drag', {
				bgPosX: bgPosX,
				bgPosY: bgPosY,
				xShift: xShift,
				yShift: yShift
			});

			previousEvent = e;
			updateBgStyle();
		}

		function removeDrag() {
			document.removeEventListener('mouseup', removeDrag);
			document.removeEventListener('mousemove', drag);
		}

		// Make the background draggable
		function draggable(e) {
			e.preventDefault();
			previousEvent = e;
			document.addEventListener('mousemove', drag);
			document.addEventListener('mouseup', removeDrag);
		}

		function load() {
			if (img.src === cachedDataUrl) return;

			var computedStyle = window.getComputedStyle(img, null);

			width = parseInt(computedStyle.width, 10);
			height = parseInt(computedStyle.height, 10);
			bgWidth = width;
			bgHeight = height;
			bgPosX = 0;
			bgPosY = 0;

			setSrcToBackground(img);

			img.style.backgroundSize =  width+'px '+height+'px';
			img.style.backgroundPosition = '0 0';
			img.addEventListener('wheelzoom.reset', reset);
			img.addEventListener('wheelzoom.destroy', destroy);

			img.addEventListener('wheel', onwheel);
			img.addEventListener('mousedown', draggable);
		}

		var destroy = function (originalProperties) {
			img.removeEventListener('wheelzoom.destroy', destroy);
			img.removeEventListener('wheelzoom.reset', reset);
			img.removeEventListener('load', load);
			img.removeEventListener('mouseup', removeDrag);
			img.removeEventListener('mousemove', drag);
			img.removeEventListener('mousedown', draggable);
			img.removeEventListener('wheel', onwheel);

			img.style.backgroundImage = originalProperties.backgroundImage;
			img.style.backgroundRepeat = originalProperties.backgroundRepeat;
			img.src = originalProperties.src;
		}.bind(null, {
			backgroundImage: img.style.backgroundImage,
			backgroundRepeat: img.style.backgroundRepeat,
			src: img.src
		});

		img.addEventListener('wheelzoom.destroy', destroy);

		options = options || {};

		Object.keys(defaults).forEach(function(key){
			settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
		});

		if (img.complete) {
			load();
		}

		img.addEventListener('load', load);
	};

	// Do nothing in IE8
	if (typeof window.getComputedStyle !== 'function') {
		return function(elements) {
			return elements;
		};
	} else {
		return function(elements, options) {
			if (elements && elements.length) {
				Array.prototype.forEach.call(elements, main, options);
			} else if (elements && elements.nodeName) {
				main(elements, options);
			}
			return elements;
		};
	}
}());<commit_msg>Bug fixing<commit_after>/*!
	Wheelzoom 1.0.0
	license: MIT
	https://github.com/Luperi/wheelzoom
*/

// create CustomEvent to override window.Event in unsupported browsers
(function () {
	function CustomEvent ( event, params ) {
		params = params || { bubbles: false, cancelable: false, detail: undefined };
		var evt = document.createEvent( 'CustomEvent' );
		evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
		return evt;
	}

	CustomEvent.prototype = window.Event.prototype;
	window.CustomEvent = CustomEvent;
})();

// create cross browser triggerEvent function
window.triggerEvent = function(target, eventName, params) {
	if (params) {
		var e = new CustomEvent(eventName, {
			'detail': params
		});
	} else {
		try {
			var e = new Event(eventName);
		} catch (err) {
			var e = new CustomEvent(eventName);
		}
	}

	return target.dispatchEvent(e, params);
};

window.wheelzoom = (function(){
	var defaults = {
		zoom: 0.10,
		maxZoom: -1
	};

	var canvas = document.createElement('canvas');

	var main = function(img, options){
		if (!img || !img.nodeName || img.nodeName !== 'IMG') { return; }

		var settings = {};
		var width;
		var height;
		var bgWidth;
		var bgHeight;
		var bgPosX;
		var bgPosY;
		var previousEvent;
		var cachedDataUrl;

		function setSrcToBackground(img) {
			img.style.backgroundImage = 'url("'+img.src+'")';
			img.style.backgroundRepeat = 'no-repeat';
			canvas.width = img.naturalWidth;
			canvas.height = img.naturalHeight;
			cachedDataUrl = canvas.toDataURL();
			img.src = cachedDataUrl;
		}

		function updateBgStyle() {
			if (bgPosX > 0) {
				bgPosX = 0;
			} else if (bgPosX < width - bgWidth) {
				bgPosX = width - bgWidth;
			}

			if (bgPosY > 0) {
				bgPosY = 0;
			} else if (bgPosY < height - bgHeight) {
				bgPosY = height - bgHeight;
			}

			img.style.backgroundSize = bgWidth+'px '+bgHeight+'px';
			img.style.backgroundPosition = bgPosX+'px '+bgPosY+'px';
		}

		function reset() {
			bgWidth = width;
			bgHeight = height;
			bgPosX = bgPosY = 0;
			updateBgStyle();
		}

		img.doZoom = function (deltaY, offsetX, offsetY, propagate) {
			if (typeof propagate === 'undefined') {
				propagate = true;
			}

			// Record the offset between the bg edge and cursor:
			var bgCursorX = offsetX - bgPosX;
			var bgCursorY = offsetY - bgPosY;
			// Use the previous offset to get the percent offset between the bg edge and cursor:
			var bgRatioX = bgCursorX/bgWidth;
			var bgRatioY = bgCursorY/bgHeight;

			// Update the bg size:
			if (deltaY < 0) {
				if (settings.maxZoom == -1 || (bgWidth + bgWidth*settings.zoom) / width <= settings.maxZoom) {
					bgWidth += bgWidth*settings.zoom;
					bgHeight += bgHeight*settings.zoom;
				}
			} else {
				bgWidth -= bgWidth*settings.zoom;
				bgHeight -= bgHeight*settings.zoom;
			}

			// Take the percent offset and apply it to the new size:
			bgPosX = offsetX - (bgWidth * bgRatioX);
			bgPosY = offsetY - (bgHeight * bgRatioY);

			if (propagate) {
				if (deltaY < 0) {
					// setTimeout to handle lot of events fired
					setTimeout(function() {
						triggerEvent(img, 'wheelzoom.in', {
							bgWidth: bgWidth,
							bgHeight: bgHeight,
							bgPosX: bgPosX,
							bgPosY: bgPosY,
							width: width,
							height: height,
							bgCursorX: bgCursorX,
							bgCursorY: bgCursorY
						});
					}, 10);
				} else {
					// setTimeout to handle lot of events fired
					setTimeout(function() {
						triggerEvent(img, 'wheelzoom.out', {
							bgWidth: bgWidth,
							bgHeight: bgHeight,
							bgPosX: bgPosX,
							bgPosY: bgPosY,
							width: width,
							height: height,
							bgCursorX: bgCursorX,
							bgCursorY: bgCursorY
						});
					}, 10);
				}
			}

			// Prevent zooming out beyond the starting size
			if (bgWidth <= width || bgHeight <= height) {
				triggerEvent(img, 'wheelzoom.reset');
			} else {
				updateBgStyle();
			}
		}

		function onwheel(e) {
			var deltaY = 0;

			e.preventDefault();

			if (e.deltaY) { // FireFox 17+ (IE9+, Chrome 31+?)
				deltaY = e.deltaY;
			} else if (e.wheelDelta) {
				deltaY = -e.wheelDelta;
			}

			// As far as I know, there is no good cross-browser way to get the cursor position relative to the event target.
			// We have to calculate the target element's position relative to the document, and subtrack that from the
			// cursor's position relative to the document.
			var rect = img.getBoundingClientRect();
			var offsetX = e.pageX - rect.left - window.pageXOffset;
			var offsetY = e.pageY - rect.top - window.pageYOffset;

			img.doZoom(deltaY, offsetX, offsetY);
		}

		img.doDrag = function (xShift, yShift) {
			bgPosX += xShift;
			bgPosY += yShift;

			updateBgStyle();
		}

		function drag(e) {
			e.preventDefault();
			var xShift = e.pageX - previousEvent.pageX;
			var yShift = e.pageY - previousEvent.pageY;

			img.doDrag(xShift, yShift);

			triggerEvent(img, 'wheelzoom.drag', {
				bgPosX: bgPosX,
				bgPosY: bgPosY,
				xShift: xShift,
				yShift: yShift
			});

			previousEvent = e;
			updateBgStyle();
		}

		function removeDrag() {
			document.removeEventListener('mouseup', removeDrag);
			document.removeEventListener('mousemove', drag);
		}

		// Make the background draggable
		function draggable(e) {
			e.preventDefault();
			previousEvent = e;
			document.addEventListener('mousemove', drag);
			document.addEventListener('mouseup', removeDrag);
		}

		function load() {
			if (img.src === cachedDataUrl) return;

			var computedStyle = window.getComputedStyle(img, null);

			width = parseInt(computedStyle.width, 10);
			height = parseInt(computedStyle.height, 10);
			bgWidth = width;
			bgHeight = height;
			bgPosX = 0;
			bgPosY = 0;

			setSrcToBackground(img);

			img.style.backgroundSize =  width+'px '+height+'px';
			img.style.backgroundPosition = '0 0';
			img.addEventListener('wheelzoom.reset', reset);
			img.addEventListener('wheelzoom.destroy', destroy);

			img.addEventListener('wheel', onwheel);
			img.addEventListener('mousedown', draggable);
		}

		var destroy = function (originalProperties) {
			img.removeEventListener('wheelzoom.destroy', destroy);
			img.removeEventListener('wheelzoom.reset', reset);
			img.removeEventListener('load', load);
			img.removeEventListener('mouseup', removeDrag);
			img.removeEventListener('mousemove', drag);
			img.removeEventListener('mousedown', draggable);
			img.removeEventListener('wheel', onwheel);

			img.style.backgroundImage = originalProperties.backgroundImage;
			img.style.backgroundRepeat = originalProperties.backgroundRepeat;
			img.src = originalProperties.src;
		}.bind(null, {
			backgroundImage: img.style.backgroundImage,
			backgroundRepeat: img.style.backgroundRepeat,
			src: img.src
		});

		img.addEventListener('wheelzoom.destroy', destroy);

		options = options || {};

		Object.keys(defaults).forEach(function(key){
			settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
		});

		if (img.complete) {
			load();
		}

		img.addEventListener('load', load);
	};

	// Do nothing in IE8
	if (typeof window.getComputedStyle !== 'function') {
		return function(elements) {
			return elements;
		};
	} else {
		return function(elements, options) {
			if (elements && elements.length) {
				elements.forEach(function(element) {
					main(element, options);
				});
			} else if (elements && elements.nodeName) {
				main(elements, options);
			}

			return elements;
		};
	}
}());<|endoftext|>


<commit_before>// Copyright 2017, University of Colorado Boulder

/**
 * A trait for subtypes of Node, used to make the node behave like a 'number' input with assistive technology.
 * An accessible number tweaker behaves like:
 *
 * - Arrow keys increment/decrement the value by a specified step size.
 * - Page Up and Page Down increments/decrements value by an alternative step size, usually larger than default.
 * - Home key sets value to its minimum.
 * - End key sets value to its maximum.
 *
 * This number tweaker is different than typical 'number' inputs because it does not support number key control. All
 * changes go through the arrow, page up, page down, home, and end keys.
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 * @author Michael Barlow (PhET Interactive Simulations)
 */

define( function( require ) {
  'use strict';

  // modules
  var CallbackTimer = require( 'SUN/CallbackTimer' );
  var Emitter = require( 'AXON/Emitter' );
  var extend = require( 'PHET_CORE/extend' );
  var inheritance = require( 'PHET_CORE/inheritance' );
  var KeyboardUtil = require( 'SCENERY/accessibility/KeyboardUtil' );
  var Node = require( 'SCENERY/nodes/Node' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var sun = require( 'SUN/sun' );
  var Util = require( 'DOT/Util' );

  var AccessibleNumberTweaker = {

    /**
     * Implement functionality for a number tweaker.
     * @public
     * @trait
     *
     * @param {function} type - The type (constructor) whose prototype we'll modify.
     */
    mixInto: function( type ) {
      assert && assert( _.includes( inheritance( type ), Node ) );

      var proto = type.prototype;

      extend( proto, {

        /**
         * This should be called in the constructor to initialize the accessible input features for the node.
         *
         * @param {Property.<number>} valueProperty
         * @param {Property.<Range>} enabledRangeProperty
         * @param {Property.<boolean>} enabledProperty
         * @param {Object} [options]
         *
         * @protected
         */
        initializeAccessibleNumberTweaker: function( valueProperty, enabledRangeProperty, enabledProperty, options ) {
          var self = this;

          var defaults = {
            a11yValueDelta: 1, // {number} - value changes by this quantity when using arrow keys
            a11yPageValueDelta: 2, // {number} - value changes by this quantity when using page up/page down

            a11yUseTimer: true, // if false value will change every native DOM keydown event, following options meaningless
            timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
            timerInterval: 100, // fire continuously at this frequency (milliseconds),

            tagName: 'input',
            inputType: 'number',
            ariaRole: 'spinbutton',
            useAriaLabel: true, // the label should be inline with aria-label attribute
            a11yValuePattern: '{{value}}', // {string} if you want units or additional content, add to pattern
            a11yDecimalPlaces: 0, // number of decimal places for the value read by assistive technology

            // Converts a value to a string, for extra formatting for the value read by the screen reader
            a11yFormatValue: function( value ) {
              return Util.toFixed( value, options.a11yDecimalPlaces );
            }
          };
          options = _.extend( {}, defaults, options );

          // Some options were already mutated in the constructor, only apply the accessibility-specific options here
          // so options are not doubled up, see https://github.com/phetsims/sun/issues/330
          var optionsToMutate = _.pick( options, _.keys( defaults ) );
          this.mutate( optionsToMutate );

          // @private {Property.<number>}
          this._valueProperty = valueProperty;

          // @private {Property.<Range>}
          this._enabledRangeProperty = enabledRangeProperty;

          // @private{Property.<boolean>}
          this._enabledProperty = enabledProperty;

          // @private {number}
          this._a11yValueDelta = options.a11yValueDelta;

          // @private {number}
          this._a11yPageValueDelta = options.a11yPageValueDelta;

          // @private {boolean}
          this._a11yUseTimer = options.a11yUseTimer;

          // @private - manages timing must be disposed
          this._callbackTimer = new CallbackTimer( {
            delay: options.timerDelay,
            interval: options.timerInterval
          } );

          // @private {Emitter} emit events when increment/decrement keys are pressed down/up
          this.incrementDownEmitter = new Emitter();
          this.decrementDownEmitter = new Emitter();

          // @private {Emitter} - emit events when value is incremented/decremented
          this.valueIncrementEmitter = new Emitter();
          this.valueDecrementEmitter = new Emitter();

          // update enabled number range for AT, required for screen reader events to behave correctly, must be disposed
          var enabledRangeObserver = function( enabledRange ) {
            self.setAccessibleAttribute( 'min', enabledRange.min );
            self.setAccessibleAttribute( 'max', enabledRange.max );
          };
          this._enabledRangeProperty.link( enabledRangeObserver );

          // a callback that is added and removed from the timer depending on keystate
          var downCallback = null;

          // @public (a11y) - handle all accessible event input
          var accessibleInputListener = this.addAccessibleInputListener( {
            keydown: function( event ) {
              self.emitKeyState( event, true );

              // if using the timer, handle update at interval
              if ( self._a11yUseTimer ) {
                if ( !self._callbackTimer.isRunning() ) {
                  self.handleKeyDown( event );

                  downCallback = self.handleKeyDown.bind( self, event );
                  self._callbackTimer.addCallback( downCallback );
                  self._callbackTimer.start();
                }
              }
              else {
                self.handleKeyDown( event );
              }
            },
            keyup: function( event ) {
              self.emitKeyState( event, false );

              if ( self._a11yUseTimer ) {
                self._callbackTimer.stop( false );
                self._callbackTimer.removeCallback( downCallback );
              }
            }
          } );

          // when the property changes, be sure to update the accessible input value
          var accessiblePropertyListener = function( value ) {
            self.inputValue = value;

            // format the value text for reading by screen reader
            var formattedValue = options.a11yFormatValue( value );
            var valueText = StringUtils.fillIn( options.a11yValuePattern, {
              value: formattedValue
            } );
            self.setAccessibleAttribute( 'aria-valuetext', valueText );
          };
          this._valueProperty.link( accessiblePropertyListener );

          // @private - called by disposeAccessibleNumberTweaker to prevent memory leaks
          this._disposeAccessibleNumberTweaker = function() {
            self._valueProperty.unlink( accessiblePropertyListener );
            self._enabledRangeProperty.unlink( enabledRangeObserver );
            self._callbackTimer.dispose();

            // emitters owned by this instance, can be disposed here
            self.incrementDownEmitter.dispose();
            self.decrementDownEmitter.dispose();
            self.valueIncrementEmitter.dispose();
            self.valueDecrementEmitter.dispose();

            self.removeAccessibleInputListener( accessibleInputListener );
          };
        },

        /**
         * Emit events related to the keystate of the tweaker. Typically used to style the tweaker during keyboard
         * interaction.
         * @private
         *
         * @param {UIEvent} event - the DOM event that was triggered
         * @param {boolean} isDown - whether or not event was triggered from down or up keys
         */
        emitKeyState: function( event, isDown ) {
          if ( event.keyCode === KeyboardUtil.KEY_UP_ARROW || event.keyCode === KeyboardUtil.KEY_RIGHT_ARROW ) {
            this.incrementDownEmitter.emit1( isDown );
          }
          else if ( event.keyCode === KeyboardUtil.KEY_DOWN_ARROW || event.keyCode === KeyboardUtil.KEY_LEFT_ARROW ) {
            this.decrementDownEmitter.emit1( isDown );
          }
        },

        /**
         * Handle the keydown event so that this node behaves like an accessible number input.
         * @private
         *
         * @param {DOMEvent} event
         */
        handleKeyDown: function( event ) {
          var code = event.keyCode;

          if ( this._enabledProperty.get() ) {

            // prevent user from changing value with number or the space keys, handle arrow keys on our own
            if ( KeyboardUtil.isArrowKey( code ) || KeyboardUtil.isNumberKey( code ) || code === KeyboardUtil.KEY_SPACE ) {
              event.preventDefault();
            }

            // handle the event
            if ( code === KeyboardUtil.KEY_RIGHT_ARROW || code === KeyboardUtil.KEY_UP_ARROW ) {
              this.valueIncrementEmitter.emit();
              this._valueProperty.set( Math.min( this._valueProperty.get() + this._a11yValueDelta, this._enabledRangeProperty.get().max ) );
            }
            else if ( code === KeyboardUtil.KEY_LEFT_ARROW || code === KeyboardUtil.KEY_DOWN_ARROW ) {
              this.valueDecrementEmitter.emit();
              this._valueProperty.set( Math.max( this._valueProperty.get() - this._a11yValueDelta, this._enabledRangeProperty.get().min ) );
            }
            else if ( code === KeyboardUtil.KEY_PAGE_UP ) {
              this._valueProperty.set( Math.min( this._valueProperty.get() + this._a11yPageValueDelta, this._enabledRangeProperty.get().max ) );
            }
            else if ( code === KeyboardUtil.KEY_PAGE_DOWN ) {
              this._valueProperty.set( Math.max( this._valueProperty.get() - this._a11yPageValueDelta, this._enabledRangeProperty.get().min ) );
            }
            else if ( code === KeyboardUtil.KEY_HOME ) {
              this._valueProperty.set( this._enabledRangeProperty.get().min );
            }
            else if ( code === KeyboardUtil.KEY_END ) {
              this._valueProperty.set( this._enabledRangeProperty.get().max );
            }
          }
        },

        /**
         * Make the accessibility related aspects of this node eligible for garbage collection. Call when disposing
         * the type that this trait is mixed into.
         * @public
         */
        disposeAccessibleNumberTweaker: function() {
          this._disposeAccessibleNumberTweaker();
        }
      } );
    }
  };

  sun.register( 'AccessibleNumberTweaker', AccessibleNumberTweaker );

  return AccessibleNumberTweaker;
} );
<commit_msg>Custom solution for accessible NumberPicker/NumberSpinner, see phetsims/gravity-force-lab-basics#62<commit_after>// Copyright 2017, University of Colorado Boulder

/**
 * A trait for subtypes of Node, used to make the node behave like a 'number' input with assistive technology.
 * An accessible number tweaker behaves like:
 *
 * - Arrow keys increment/decrement the value by a specified step size.
 * - Page Up and Page Down increments/decrements value by an alternative step size, usually larger than default.
 * - Home key sets value to its minimum.
 * - End key sets value to its maximum.
 *
 * This number tweaker is different than typical 'number' inputs because it does not support number key control. All
 * changes go through the arrow, page up, page down, home, and end keys.
 *
 * Screen reader support for aria-valuetext on spin buttons is too poor to use. Until there is better support, we
 * are using alerts to read the value with units at the right time, see
 * https://github.com/phetsims/gravity-force-lab-basics/issues/62
 *
 * @author Jesse Greenberg (PhET Interactive Simulations)
 * @author Michael Barlow (PhET Interactive Simulations)
 */

define( function( require ) {
  'use strict';

  // modules
  var CallbackTimer = require( 'SUN/CallbackTimer' );
  var Emitter = require( 'AXON/Emitter' );
  var extend = require( 'PHET_CORE/extend' );
  var inheritance = require( 'PHET_CORE/inheritance' );
  var KeyboardUtil = require( 'SCENERY/accessibility/KeyboardUtil' );
  var Node = require( 'SCENERY/nodes/Node' );
  var StringUtils = require( 'PHETCOMMON/util/StringUtils' );
  var sun = require( 'SUN/sun' );
  var utteranceQueue = require( 'SCENERY_PHET/accessibility/utteranceQueue' );
  var Util = require( 'DOT/Util' );

  var AccessibleNumberTweaker = {

    /**
     * Implement functionality for a number tweaker.
     * @public
     * @trait
     *
     * @param {function} type - The type (constructor) whose prototype we'll modify.
     */
    mixInto: function( type ) {
      assert && assert( _.includes( inheritance( type ), Node ) );

      var proto = type.prototype;

      extend( proto, {

        /**
         * This should be called in the constructor to initialize the accessible input features for the node.
         *
         * @param {Property.<number>} valueProperty
         * @param {Property.<Range>} enabledRangeProperty
         * @param {Property.<boolean>} enabledProperty
         * @param {Object} [options]
         *
         * @protected
         */
        initializeAccessibleNumberTweaker: function( valueProperty, enabledRangeProperty, enabledProperty, options ) {
          var self = this;

          var defaults = {
            a11yValueDelta: 1, // {number} - value changes by this quantity when using arrow keys
            a11yPageValueDelta: 2, // {number} - value changes by this quantity when using page up/page down

            a11yUseTimer: true, // if false value will change every native DOM keydown event, following options meaningless
            timerDelay: 400, // start to fire continuously after pressing for this long (milliseconds)
            timerInterval: 100, // fire continuously at this frequency (milliseconds),

            tagName: 'button',
            parentContainerTagName: 'div',
            ariaRole: 'spinbutton',
            prependLabels: true,
            focusable: true,
            labelTagName: 'p',
            a11yValuePattern: '{{value}}', // {string} if you want units or additional content, add to pattern
            a11yDecimalPlaces: 0, // number of decimal places for the value read by assistive technology

            // Converts a value to a string, for extra formatting for the value read by the screen reader
            a11yFormatValue: function( value ) {
              return Util.toFixed( value, options.a11yDecimalPlaces );
            }
          };
          options = _.extend( {}, defaults, options );

          // Some options were already mutated in the constructor, only apply the accessibility-specific options here
          // so options are not doubled up, see https://github.com/phetsims/sun/issues/330
          var optionsToMutate = _.pick( options, _.keys( defaults ) );
          this.mutate( optionsToMutate );

          // this number tweaker is "aria-labelledby" its own label, meaning that the label element content will be
          // read on focus
          this.setAriaLabelledByNode( this );
          this.setAriaLabelContent( 'LABEL' );
          this.setAccessibleAttribute( 'aria-roledescription', 'spinbutton' );

          // @private {Property.<number>}
          this._valueProperty = valueProperty;

          // @private {Property.<Range>}
          this._enabledRangeProperty = enabledRangeProperty;

          // @private{Property.<boolean>}
          this._enabledProperty = enabledProperty;

          // @private {number}
          this._a11yValueDelta = options.a11yValueDelta;

          // @private {number}
          this._a11yPageValueDelta = options.a11yPageValueDelta;

          // @private {boolean}
          this._a11yUseTimer = options.a11yUseTimer;

          // @private - manages timing must be disposed
          this._callbackTimer = new CallbackTimer( {
            delay: options.timerDelay,
            interval: options.timerInterval
          } );

          // @private {Emitter} emit events when increment/decrement keys are pressed down/up
          this.incrementDownEmitter = new Emitter();
          this.decrementDownEmitter = new Emitter();

          // @private {Emitter} - emit events when value is incremented/decremented
          this.valueIncrementEmitter = new Emitter();
          this.valueDecrementEmitter = new Emitter();

          // update enabled number range for AT, required for screen reader events to behave correctly, must be disposed
          var enabledRangeObserver = function( enabledRange ) {
            self.setAccessibleAttribute( 'min', enabledRange.min );
            self.setAccessibleAttribute( 'max', enabledRange.max );
          };
          this._enabledRangeProperty.link( enabledRangeObserver );

          // a callback that is added and removed from the timer depending on keystate
          var downCallback = null;

          // @public (a11y) - handle all accessible event input
          var accessibleInputListener = this.addAccessibleInputListener( {
            keydown: function( event ) {
              self.emitKeyState( event, true );

              // if using the timer, handle update at interval
              if ( self._a11yUseTimer ) {
                if ( !self._callbackTimer.isRunning() ) {
                  self.handleKeyDown( event );

                  downCallback = self.handleKeyDown.bind( self, event );
                  self._callbackTimer.addCallback( downCallback );
                  self._callbackTimer.start();
                }
              }
              else {
                self.handleKeyDown( event );
              }
            },
            keyup: function( event ) {
              self.emitKeyState( event, false );

              if ( self._a11yUseTimer ) {
                self._callbackTimer.stop( false );
                self._callbackTimer.removeCallback( downCallback );
              }
            },
            focus: function( event ) {

              // on focus, read the current value of the input
              var formattedValue = options.a11yFormatValue( self._valueProperty.get() );
              readValue( formattedValue, options.a11yValuePattern );
            }
          } );

          // when the property changes, be sure to update the accessible input value
          var accessiblePropertyListener = function( value ) {
            var formattedValue = options.a11yFormatValue( value );
            readValue( formattedValue, options.a11yValuePattern );
          };
          this._valueProperty.link( accessiblePropertyListener );

          // @private - called by disposeAccessibleNumberTweaker to prevent memory leaks
          this._disposeAccessibleNumberTweaker = function() {
            self._valueProperty.unlink( accessiblePropertyListener );
            self._enabledRangeProperty.unlink( enabledRangeObserver );
            self._callbackTimer.dispose();

            // emitters owned by this instance, can be disposed here
            self.incrementDownEmitter.dispose();
            self.decrementDownEmitter.dispose();
            self.valueIncrementEmitter.dispose();
            self.valueDecrementEmitter.dispose();

            self.removeAccessibleInputListener( accessibleInputListener );
          };
        },

        /**
         * Emit events related to the keystate of the tweaker. Typically used to style the tweaker during keyboard
         * interaction.
         * @private
         *
         * @param {UIEvent} event - the DOM event that was triggered
         * @param {boolean} isDown - whether or not event was triggered from down or up keys
         */
        emitKeyState: function( event, isDown ) {
          if ( event.keyCode === KeyboardUtil.KEY_UP_ARROW || event.keyCode === KeyboardUtil.KEY_RIGHT_ARROW ) {
            this.incrementDownEmitter.emit1( isDown );
          }
          else if ( event.keyCode === KeyboardUtil.KEY_DOWN_ARROW || event.keyCode === KeyboardUtil.KEY_LEFT_ARROW ) {
            this.decrementDownEmitter.emit1( isDown );
          }
        },

        /**
         * Handle the keydown event so that this node behaves like an accessible number input.
         * @private
         *
         * @param {DOMEvent} event
         */
        handleKeyDown: function( event ) {
          var code = event.keyCode;

          if ( this._enabledProperty.get() ) {

            // prevent user from changing value with number or the space keys, handle arrow keys on our own
            if ( KeyboardUtil.isArrowKey( code ) || KeyboardUtil.isNumberKey( code ) || code === KeyboardUtil.KEY_SPACE ) {
              event.preventDefault();
            }

            // handle the event
            if ( code === KeyboardUtil.KEY_RIGHT_ARROW || code === KeyboardUtil.KEY_UP_ARROW ) {
              this.valueIncrementEmitter.emit();
              this._valueProperty.set( Math.min( this._valueProperty.get() + this._a11yValueDelta, this._enabledRangeProperty.get().max ) );
            }
            else if ( code === KeyboardUtil.KEY_LEFT_ARROW || code === KeyboardUtil.KEY_DOWN_ARROW ) {
              this.valueDecrementEmitter.emit();
              this._valueProperty.set( Math.max( this._valueProperty.get() - this._a11yValueDelta, this._enabledRangeProperty.get().min ) );
            }
            else if ( code === KeyboardUtil.KEY_PAGE_UP ) {
              this._valueProperty.set( Math.min( this._valueProperty.get() + this._a11yPageValueDelta, this._enabledRangeProperty.get().max ) );
            }
            else if ( code === KeyboardUtil.KEY_PAGE_DOWN ) {
              this._valueProperty.set( Math.max( this._valueProperty.get() - this._a11yPageValueDelta, this._enabledRangeProperty.get().min ) );
            }
            else if ( code === KeyboardUtil.KEY_HOME ) {
              this._valueProperty.set( this._enabledRangeProperty.get().min );
            }
            else if ( code === KeyboardUtil.KEY_END ) {
              this._valueProperty.set( this._enabledRangeProperty.get().max );
            }
          }
        },

        /**
         * Make the accessibility related aspects of this node eligible for garbage collection. Call when disposing
         * the type that this trait is mixed into.
         * @public
         */
        disposeAccessibleNumberTweaker: function() {
          this._disposeAccessibleNumberTweaker();
        }
      } );
    }
  };

  /**
   * Read the value, surrounding the readout with optional text that can give the value context. By default, the text
   * will be "{{value}}", but it could be changed to something like "{{value}} billion kg", for instance.
   *
   * @param {string} formattedValue
   * @param {string} pattern
   * @return {string}
   */
  function readValue( formattedValue, pattern ) {
    var valueText = StringUtils.fillIn( pattern, {
      value: formattedValue
    } );
    utteranceQueue.addToBack( valueText );
  }

  sun.register( 'AccessibleNumberTweaker', AccessibleNumberTweaker );

  return AccessibleNumberTweaker;
} );
<|endoftext|>


<commit_before>// Copyright 2016, University of Colorado Boulder

/**
 * Generates the PhET-iO API Type documentation which will be shown at https://phet-io.colorado.edu/devguide/
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 */
/* eslint-env node */
'use strict';
var requirejs = require( '../../../node_modules/requirejs/bin/r.js' ); // eslint-disable-line require-statement-match
var fs = require( 'fs' );

// Mock up window globals for running in node mode
global.assert = require( 'assert' );
global._ = require( '../../../sherpa/lib/lodash-4.17.4.js' );
global.phet = {
  chipper: {
    queryParameters: {}
  },
  phetio: {
    queryParameters: {
      phetioExpressions: '[]'
    }
  }
};

global.window = global; // eslint-disable-line no-native-reassign

var assert = global.assert;

requirejs.config( {
  paths: {
    text: '../../../sherpa/lib/text-2.0.12',
    AXON: '../../../axon/js',
    REPOSITORY: '../..',
    PHET_CORE: '../../../phet-core/js',
    PHETCOMMON: '../../../phetcommon/js',
    PHET_IO: '../../../phet-io/js',
    TANDEM: '../../../tandem/js'
  }
} );

var TObject = requirejs( 'PHET_IO/types/TObject' );

// Get a list of all of the types in the 'types' directory
var walkSync = function( dir, filelist ) {
  var files = fs.readdirSync( dir );
  filelist = filelist || [];
  files.forEach( function( file ) {
    if ( fs.statSync( dir + '/' + file ).isDirectory() ) {
      filelist = walkSync( dir + '/' + file, filelist );
    }
    else {
      filelist.push( dir + '/' + file.replace( '.js', '' ) );
    }
  } );
  return filelist;
};

var toHTML = function( json ) {
  var html = '';
  var types = _.sortBy( _.keys( json ), function( key ) {
    return key;
  } );
  for ( var i = 0; i < types.length; i++ ) {

    var typeName = types[ i ];
    var wrapperType = json[ typeName ];
    var methods = '';
    var sortedMethodNames = _.sortBy( _.keys( wrapperType.methods ), function( key ) {
      return key;
    } );
    for ( var k = 0; k < sortedMethodNames.length; k++ ) {
      var methodName = sortedMethodNames[ k ];
      var method = wrapperType.methods[ methodName ];
      var params = method.parameterTypes.map( function( p ) {
        assert && assert( p.typeName, 'Parameter was not a type in toHTML: ' + typeName + '.' + methodName );
        return p.typeName;
      } );
      methods += '
' + methodName + ': (' + params + ') ➞ ' + method.returnType.typeName + '
' +
                 '
' + method.documentation + '
';
    }
    var eventsString = wrapperType.events ? 'events: ' + ( wrapperType.events || '' ) + '' : '';

    var supertype = wrapperType.supertype;
    html = html + '
' + typeName + ' ' +
           (supertype.typeName ? '(extends ' + supertype.typeName + ')' : '') +
           '
' +
           '
' +
           '' + wrapperType.documentation + '
' +
           eventsString +
           '
' + methods + '

';
  }
  return html;
};

module.exports = function(callback) {
  var allFiles = walkSync( '../types', [] );
  var result = {};

  // Load the files using the same strings as they are loaded in other files
  // This way they aren't loaded twice.
  // substring(3) to remove the '../' before the types directory
  var filesToGet = allFiles.map( function( elm ) {return 'PHET_IO/' + elm.substring( 3 );} );

  requirejs( filesToGet, function( /* arguments */ ) {
    var args = Array.prototype.slice.call( arguments );
    for ( var i = 0; i < args.length; i++ ) {
      var arg = args[ i ];
      var filename = filesToGet[ i ];
      if ( arg.typeName ) {
        result[ arg.typeName ] = arg;
      }
      else {
        if ( filename.indexOf( 'TFunctionWrapper' ) >= 0 ) {
          var returnType = TObject;
          var parameterTypes = [ TObject ];
          var instantiatedType = arg( returnType, parameterTypes );
          result[ instantiatedType.typeName ] = instantiatedType;
        }
        else {
          var instanceType = arg( TObject );
          result[ instanceType.typeName ] = instanceType;
        }
      }
    }

    var html = toHTML( result );
    callback(html);
  });
};<commit_msg>adapted for terminal use to console.log, https://github.com/phetsims/phet-io/issues/972<commit_after>// Copyright 2016, University of Colorado Boulder

/**
 * Generates the PhET-iO API Type documentation which will be shown at https://phet-io.colorado.edu/devguide/
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Sam Reid (PhET Interactive Simulations)
 */
/* eslint-env node */
'use strict';
var requirejs = require( '../../../node_modules/requirejs/bin/r.js' ); // eslint-disable-line require-statement-match
var fs = require( 'fs' );

// Mock up window globals for running in node mode

// TODO: Why was this running with an error in TFunctionWrapper?
// global.assert = require( 'assert' );
global.assert = null;
global._ = require( '../../../sherpa/lib/lodash-4.17.4.js' );
global.phet = {
  chipper: {
    queryParameters: {}
  },
  phetio: {
    queryParameters: {
      phetioExpressions: '[]'
    }
  }
};

global.window = global; // eslint-disable-line no-native-reassign

var assert = global.assert;

requirejs.config( {
  paths: {
    text: '../../../sherpa/lib/text-2.0.12',
    AXON: '../../../axon/js',
    REPOSITORY: '../..',
    PHET_CORE: '../../../phet-core/js',
    PHETCOMMON: '../../../phetcommon/js',
    PHET_IO: '../../../phet-io/js',
    TANDEM: '../../../tandem/js'
  }
} );

var TObject = requirejs( 'PHET_IO/types/TObject' );

// Get a list of all of the types in the 'types' directory
var walkSync = function( dir, filelist ) {
  var files = fs.readdirSync( dir );
  filelist = filelist || [];
  files.forEach( function( file ) {
    if ( fs.statSync( dir + '/' + file ).isDirectory() ) {
      filelist = walkSync( dir + '/' + file, filelist );
    }
    else {
      filelist.push( dir + '/' + file.replace( '.js', '' ) );
    }
  } );
  return filelist;
};

var toHTML = function( json ) {
  var html = '';
  var types = _.sortBy( _.keys( json ), function( key ) {
    return key;
  } );
  for ( var i = 0; i < types.length; i++ ) {

    var typeName = types[ i ];
    var wrapperType = json[ typeName ];
    var methods = '';
    var sortedMethodNames = _.sortBy( _.keys( wrapperType.methods ), function( key ) {
      return key;
    } );
    for ( var k = 0; k < sortedMethodNames.length; k++ ) {
      var methodName = sortedMethodNames[ k ];
      var method = wrapperType.methods[ methodName ];
      var params = method.parameterTypes.map( function( p ) {
        assert && assert( p.typeName, 'Parameter was not a type in toHTML: ' + typeName + '.' + methodName );
        return p.typeName;
      } );
      methods += '
' + methodName + ': (' + params + ') ➞ ' + method.returnType.typeName + '
' +
                 '
' + method.documentation + '
';
    }
    var eventsString = wrapperType.events ? 'events: ' + ( wrapperType.events || '' ) + '' : '';

    var supertype = wrapperType.supertype;
    html = html + '
' + typeName + ' ' +
           (supertype.typeName ? '(extends ' + supertype.typeName + ')' : '') +
           '
' +
           '
' +
           '' + wrapperType.documentation + '
' +
           eventsString +
           '
' + methods + '

';
  }
  return html;
};

module.exports = function( callback ) {
  var allFiles = walkSync( '../types', [] );
  var result = {};

  // Load the files using the same strings as they are loaded in other files
  // This way they aren't loaded twice.
  // substring(3) to remove the '../' before the types directory
  var filesToGet = allFiles.map( function( elm ) {return 'PHET_IO/' + elm.substring( 3 );} );

  requirejs( filesToGet, function( /* arguments */ ) {
    var args = Array.prototype.slice.call( arguments );
    for ( var i = 0; i < args.length; i++ ) {
      var arg = args[ i ];
      var filename = filesToGet[ i ];
      if ( arg.typeName ) {
        result[ arg.typeName ] = arg;
      }
      else {
        if ( filename.indexOf( 'TFunctionWrapper' ) >= 0 ) {
          var returnType = TObject;
          var parameterTypes = [ TObject ];
          var instantiatedType = arg( returnType, parameterTypes );
          result[ instantiatedType.typeName ] = instantiatedType;
        }
        else {
          var instanceType = arg( TObject );
          result[ instanceType.typeName ] = instanceType;
        }
      }
    }

    var html = toHTML( result );
    callback( html );
  } );
};

// Called from the command line, and not required
if ( require.main === module ) {
  module.exports( function( html ) { console.log( html ); } );
}
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
CLASS({
  package: 'foam.net.proto',
  name: 'JSONProtoPropertyTrait',
  properties: [
    {
      name: 'protoField',
      documentation: 'Gives the protobuf field name corresponding to this ' +
          'property. If this property corresponds to a field buried in a ' +
          'nested proto, this can be an array of field names.',
      defaultValue: undefined,
      adapt: function(old, nu) {
        return typeof nu === 'string' ? [nu] : nu;
      }
    },
    {
      name: 'protoModel',
      documentation: 'If this proto field contains a nested proto message, ' +
          'the <tt>protoModel</tt> is used as the model for that proto. For ' +
          'a repeated field, each element in the array becomes an instance ' +
          'of this model.',
    },
  ],

  methods: [
    function fromProto(obj) {
      if (this.protoField) {
        var o = obj;
        for (var i = 0; i < this.protoField.length; i++) {
          if (!o) break;
          o = o[this.protoField[i]];
        }

        if (o && this.protoModel) {
          var m = this.X.lookup(this.protoModel);
          if (m) {
            var f = (function(x) {
              if (m.getPrototype().fromProto) {
                var nu = m.create(null, this.Y);
                nu.fromProto(x);
                return nu;
              } else {
                return m.create(x, this.Y);
              }
            }).bind(this);

            return Array.isArray(o) ? o.map(f) : f(o);
          }
        }

        return o;
      } else {
        console.warn('fromProto on ' + this.name + ', which has no protoField defined.');
        return undefined;
      }
    },

    function toProto(obj, value) {
      if (this.protoField) {
        var o = obj;
        if (this.protoField.length > 1) {
          for (var i = 0; i < this.protoField.length - 1; i++) {
            if (typeof o[this.protoField[i]] !== 'object') {
              o[this.protoField[i]] = {};
            }
            o = o[this.protoField[i]];
          }
        }

        // Now o is the final object we want to put our value on.
        // If there's a protoModel defined, we'll have that model serialize
        // itself to JSON proto format.
        if (this.protoModel) {
          if (Array.isArray(value)) {
            value = value.map(function(x) { return x.toProto(); });
          } else {
            value = value.toProto();
          }
        }

        o[this.protoField[this.protoField.length - 1]] = value;
      } else {
        console.warn('toProto called on ' + this.name + ' which has no protoField defined.');
      }
    },
  ]
});
<commit_msg>Protobufs: Handle integer protoFields<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
CLASS({
  package: 'foam.net.proto',
  name: 'JSONProtoPropertyTrait',
  properties: [
    {
      name: 'protoField',
      documentation: 'Gives the protobuf field name corresponding to this ' +
          'property. If this property corresponds to a field buried in a ' +
          'nested proto, this can be an array of field names.',
      defaultValue: undefined,
      adapt: function(old, nu) {
        return typeof nu === 'object' ? nu : [nu];
      }
    },
    {
      name: 'protoModel',
      documentation: 'If this proto field contains a nested proto message, ' +
          'the <tt>protoModel</tt> is used as the model for that proto. For ' +
          'a repeated field, each element in the array becomes an instance ' +
          'of this model.',
    },
  ],

  methods: [
    function fromProto(obj) {
      if (this.protoField) {
        var o = obj;
        for (var i = 0; i < this.protoField.length; i++) {
          if (!o) break;
          o = o[this.protoField[i]];
        }

        if (o && this.protoModel) {
          var m = this.X.lookup(this.protoModel);
          if (m) {
            var f = (function(x) {
              if (m.getPrototype().fromProto) {
                var nu = m.create(null, this.Y);
                nu.fromProto(x);
                return nu;
              } else {
                return m.create(x, this.Y);
              }
            }).bind(this);

            return Array.isArray(o) ? o.map(f) : f(o);
          }
        }

        return o;
      } else {
        console.warn('fromProto on ' + this.name + ', which has no protoField defined.');
        return undefined;
      }
    },

    function toProto(obj, value) {
      if (this.protoField) {
        var o = obj;
        if (this.protoField.length > 1) {
          for (var i = 0; i < this.protoField.length - 1; i++) {
            if (typeof o[this.protoField[i]] !== 'object') {
              o[this.protoField[i]] = {};
            }
            o = o[this.protoField[i]];
          }
        }

        // Now o is the final object we want to put our value on.
        // If there's a protoModel defined, we'll have that model serialize
        // itself to JSON proto format.
        if (this.protoModel) {
          if (Array.isArray(value)) {
            value = value.map(function(x) { return x.toProto(); });
          } else {
            value = value.toProto();
          }
        }

        o[this.protoField[this.protoField.length - 1]] = value;
      } else {
        console.warn('toProto called on ' + this.name + ' which has no protoField defined.');
      }
    },
  ]
});
<|endoftext|>


<commit_before>'use strict';
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2015-2016 ArangoDB GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Alan Plum
////////////////////////////////////////////////////////////////////////////////

const _ = require('lodash');
const InternalServerError = require('http-errors').InternalServerError;
const internal = require('internal');
const assert = require('assert');
const Module = require('module');
const semver = require('semver');
const path = require('path');
const fs = require('fs');
const defaultTypes = require('@arangodb/foxx/types');
const FoxxContext = require('@arangodb/foxx/context');
const parameterTypes = require('@arangodb/foxx/manager-utils').parameterTypes;
const getReadableName = require('@arangodb/foxx/manager-utils').getReadableName;
const Router = require('@arangodb/foxx/router/router');
const Tree = require('@arangodb/foxx/router/tree');

const $_MODULE_ROOT = Symbol.for('@arangodb/module.root');
const $_MODULE_CONTEXT = Symbol.for('@arangodb/module.context');

const APP_PATH = internal.appPath ? path.resolve(internal.appPath) : undefined;
const STARTUP_PATH = internal.startupPath ? path.resolve(internal.startupPath) : undefined;
const DEV_APP_PATH = internal.devAppPath ? path.resolve(internal.devAppPath) : undefined;

module.exports = class FoxxService {
  constructor(data) {
    assert(data, 'no arguments');
    assert(data.mount, 'mount path required');
    assert(data.path, `local path required for app "${data.mount}"`);
    assert(data.manifest, `called without manifest for app "${data.mount}"`);

    // mount paths always start with a slash
    this.mount = data.mount;
    this.path = data.path;
    this.basePath = path.resolve(this.root, this.path);
    this.isDevelopment = Boolean(data.isDevelopment);

    this.manifest = data.manifest;
    if (!this.manifest.dependencies) {
      this.manifest.dependencies = {};
    }
    if (!this.manifest.configuration) {
      this.manifest.configuration = {};
    }

    this.options = data.options;
    if (!this.options.configuration) {
      this.options.configuration = {};
    }
    if (!this.options.dependencies) {
      this.options.dependencies = {};
    }

    this.configuration = createConfiguration(this.manifest.configuration);
    this.dependencies = createDependencies(this.manifest.dependencies, this.options.dependencies);
    const warnings = this.applyConfiguration(this.options.configuration);
    if (warnings.length) {
      console.warnLines(
        `Stored configuration for app "${data.mount}" has errors:\n${warnings.join('\n  ')}`
      );
    }

    if (this.manifest.thumbnail) {
      const thumb = path.resolve(this.root, this.path, this.manifest.thumbnail);
      try {
        this.thumbnail = fs.read64(thumb);
      } catch (e) {
        this.thumbnail = null;
        console.warnLines(
          `Cannot read thumbnail "${thumb}" for app "${data.mount}": ${e.stack}`
        );
      }
    } else {
      this.thumbnail = null;
    }

    const range = this.manifest.engines && this.manifest.engines.arangodb;
    this.legacy = range ? semver.gtr('3.0.0', range) : false;
    if (this.legacy) {
      console.debug(
        `Running ${this.mount} in 2.x compatibility mode (requested version ${range} pre-dates 3.0.0)`
      );
    }

    this._reset();
  }

  applyConfiguration(config) {
    const definitions = this.manifest.configuration;
    const warnings = [];

    _.each(config, function (rawValue, name) {
      const def = definitions[name];
      if (!def) {
        warnings.push(`Unexpected option "${name}"`);
        return;
      }

      if (def.required === false && (rawValue === undefined || rawValue === null || rawValue === '')) {
        delete this.options.configuration[name];
        this.configuration[name] = def.default;
        return;
      }

      const validate = parameterTypes[def.type];
      let parsedValue = rawValue;
      let warning;

      if (validate.isJoi) {
        const result = validate.required().validate(rawValue);
        if (result.error) {
          warning = result.error.message.replace(/^"value"/, `"${name}"`);
        } else {
          parsedValue = result.value;
        }
      } else {
        try {
          parsedValue = validate(rawValue);
        } catch (e) {
          warning = `"${name}": ${e.message}`;
        }
      }

      if (warning) {
        warnings.push(warning);
      } else {
        this.options.configuration[name] = rawValue;
        this.configuration[name] = parsedValue;
      }
    }, this);

    return warnings;
  }

  buildRoutes() {
    const service = this;
    const tree = new Tree(this.main.context, this.router);
    this.docs = {
      swagger: '2.0',
      basePath: this.main.context.baseUrl,
      paths: tree.buildSwaggerPaths(),
      info: {
        title: this.name,
        description: this.manifest.description,
        version: this.version,
        license: {
          name: this.manifest.license
        }
      }
    };
    this.routes.routes.push({
      url: {match: '/*'},
      action: {
        callback(req, res, opts, next) {
          let handled = true;

          try {
            handled = tree.dispatch(req, res);
          } catch (e) {
            let error = e;
            if (!e.statusCode) {
              console.errorLines(e.stack);
              error = new InternalServerError();
              error.cause = e;
            }
            const body = {
              error: true,
              errorNum: error.statusCode,
              errorMessage: error.message,
              code: error.statusCode
            };
            if (error.statusCode === 405 && error.methods) {
              res.headers.allow = error.methods.join(', ');
            }
            if (service.isDevelopment) {
              let err = error.cause || error;
              body.exception = String(err);
              body.stacktrace = err.stack;
            }
            if (error.extra) {
              Object.keys(error.extra).forEach(function (key) {
                body[key] = error.extra[key];
              });
            }
            res.responseCode = error.statusCode;
            res.contentType = 'application/json';
            res.body = JSON.stringify(body);
          }

          if (!handled) {
            next();
          }
        }
      }
    });
  }

  applyDependencies(deps) {
    const definitions = this.manifest.dependencies;
    const warnings = [];

    _.each(deps, function (mount, name) {
      const dfn = definitions[name];
      if (!dfn) {
        warnings.push(`Unexpected dependency "${name}"`);
      } else {
        this.options.dependencies[name] = mount;
      }
    }, this);

    return warnings;
  }

  _PRINT(context) {
    context.output += `[FoxxService "${this.name}" (${this.version}) on ${this.mount}]`;
  }

  toJSON() {
    const result = {
      name: this.name,
      version: this.version,
      manifest: this.manifest,
      path: this.path,
      options: this.options,
      mount: this.mount,
      root: this.root,
      isSystem: this.isSystem,
      isDevelopment: this.isDevelopment
    };
    if (this.error) {
      result.error = this.error;
    }
    if (this.manifest.author) {
      result.author = this.manifest.author;
    }
    if (this.manifest.description) {
      result.description = this.manifest.description;
    }
    if (this.thumbnail) {
      result.thumbnail = this.thumbnail;
    }
    return result;
  }

  simpleJSON() {
    return {
      name: this.name,
      version: this.version,
      mount: this.mount
    };
  }

  development(isDevelopment) {
    this.isDevelopment = isDevelopment;
  }

  getConfiguration(simple) {
    const config = {};
    const definitions = this.manifest.configuration;
    const options = this.options.configuration;
    _.each(definitions, function (dfn, name) {
      const value = options[name] === undefined ? dfn.default : options[name];
      config[name] = simple ? value : _.extend({}, dfn, {
        title: getReadableName(name),
        current: value
      });
    });
    return config;
  }

  getDependencies(simple) {
    const deps = {};
    const definitions = this.manifest.dependencies;
    const options = this.options.dependencies;
    _.each(definitions, function (dfn, name) {
      deps[name] = simple ? options[name] : {
        definition: dfn,
        title: getReadableName(name),
        current: options[name]
      };
    });
    return deps;
  }

  needsConfiguration() {
    const config = this.getConfiguration();
    const deps = this.getDependencies();
    return _.any(config, function (cfg) {
      return cfg.current === undefined && cfg.required !== false;
    }) || _.any(deps, function (dep) {
      return dep.current === undefined && dep.definition.required !== false;
    });
  }

  run(filename, options) {
    options = options || {};
    filename = path.resolve(this.main[$_MODULE_CONTEXT].__dirname, filename);

    const module = new Module(filename, this.main);
    module[$_MODULE_CONTEXT].console = this.main[$_MODULE_CONTEXT].console;
    module.context = _.extend(
      new FoxxContext(this),
      this.main.context,
      options.foxxContext
    );

    if (options.context) {
      Object.keys(options.context).forEach(function (key) {
        module[$_MODULE_CONTEXT][key] = options.context[key];
      });
    }

    if (this.legacy) {
      module[$_MODULE_CONTEXT].applicationContext = module.context;
    }

    module.load(filename);
    return module.exports;
  }

  _reset() {
    this.requireCache = {};
    const lib = this.manifest.lib || '.';
    const moduleRoot = path.resolve(this.root, this.path, lib);
    const foxxConsole = require('@arangodb/foxx/console')(this.mount);
    this.main = new Module(`foxx:${this.mount}`);
    this.main.filename = path.resolve(moduleRoot, '.foxx');
    this.main[$_MODULE_ROOT] = moduleRoot;
    this.main[$_MODULE_CONTEXT].console = foxxConsole;
    this.main.require.cache = this.requireCache;
    this.main.context = new FoxxContext(this);
    this.router = new Router();
    this.types = new Map(defaultTypes);
    this.routes = {
      name: `foxx("${this.mount}")`,
      routes: []
    };

    if (this.legacy) {
      this.main.context.foxxFilename = this.main.context.fileName;
    }
  }

  get exports() {
    return this.main.exports;
  }

  get name() {
    return this.manifest.name;
  }

  get version() {
    return this.manifest.version;
  }

  get isSystem() {
    return this.mount.charAt(1) === '_';
  }

  get root() {
    if (this.isSystem) {
      return FoxxService._systemAppPath;
    }
    return FoxxService._appPath;
  }

  get collectionPrefix() {
    return this.mount.substr(1).replace(/[-.:/]/g, '_') + '_';
  }

  static get _startupPath() {
    return STARTUP_PATH;
  }

  static get _appPath() {
    return APP_PATH ? (
      path.join(APP_PATH, '_db', internal.db._name())
    ) : undefined;
  }

  static get _systemAppPath() {
    return APP_PATH ? (
      path.join(STARTUP_PATH, 'apps', 'system')
    ) : undefined;
  }

  static get _oldAppPath() {
    return APP_PATH ? (
      path.join(APP_PATH, 'databases', internal.db._name())
    ) : undefined;
  }

  static get _devAppPath() {
    return DEV_APP_PATH ? (
      path.join(DEV_APP_PATH, 'databases', internal.db._name())
    ) : undefined;
  }
};

function createConfiguration(definitions) {
  const config = {};
  Object.keys(definitions).forEach(function (name) {
    const def = definitions[name];
    if (def.default !== undefined) {
      config[name] = def.default;
    }
  });
  return config;
}

function createDependencies(definitions, options) {
  const deps = {};
  Object.keys(definitions).forEach(function (name) {
    Object.defineProperty(deps, name, {
      configurable: true,
      enumerable: true,
      get() {
        const mount = options[name];
        return mount ? require('@arangodb/foxx').getExports(mount) : null;
      }
    });
  });
  return deps;
}
<commit_msg>Service#thumbnail should use path instead of base64<commit_after>'use strict';
////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2015-2016 ArangoDB GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Alan Plum
////////////////////////////////////////////////////////////////////////////////

const _ = require('lodash');
const InternalServerError = require('http-errors').InternalServerError;
const internal = require('internal');
const assert = require('assert');
const Module = require('module');
const semver = require('semver');
const path = require('path');
const fs = require('fs');
const defaultTypes = require('@arangodb/foxx/types');
const FoxxContext = require('@arangodb/foxx/context');
const parameterTypes = require('@arangodb/foxx/manager-utils').parameterTypes;
const getReadableName = require('@arangodb/foxx/manager-utils').getReadableName;
const Router = require('@arangodb/foxx/router/router');
const Tree = require('@arangodb/foxx/router/tree');

const $_MODULE_ROOT = Symbol.for('@arangodb/module.root');
const $_MODULE_CONTEXT = Symbol.for('@arangodb/module.context');

const APP_PATH = internal.appPath ? path.resolve(internal.appPath) : undefined;
const STARTUP_PATH = internal.startupPath ? path.resolve(internal.startupPath) : undefined;
const DEV_APP_PATH = internal.devAppPath ? path.resolve(internal.devAppPath) : undefined;

module.exports = class FoxxService {
  constructor(data) {
    assert(data, 'no arguments');
    assert(data.mount, 'mount path required');
    assert(data.path, `local path required for app "${data.mount}"`);
    assert(data.manifest, `called without manifest for app "${data.mount}"`);

    // mount paths always start with a slash
    this.mount = data.mount;
    this.path = data.path;
    this.basePath = path.resolve(this.root, this.path);
    this.isDevelopment = Boolean(data.isDevelopment);

    this.manifest = data.manifest;
    if (!this.manifest.dependencies) {
      this.manifest.dependencies = {};
    }
    if (!this.manifest.configuration) {
      this.manifest.configuration = {};
    }

    this.options = data.options;
    if (!this.options.configuration) {
      this.options.configuration = {};
    }
    if (!this.options.dependencies) {
      this.options.dependencies = {};
    }

    this.configuration = createConfiguration(this.manifest.configuration);
    this.dependencies = createDependencies(this.manifest.dependencies, this.options.dependencies);
    const warnings = this.applyConfiguration(this.options.configuration);
    if (warnings.length) {
      console.warnLines(
        `Stored configuration for app "${data.mount}" has errors:\n${warnings.join('\n  ')}`
      );
    }

    this.thumbnail = null;
    if (this.manifest.thumbnail) {
      const thumb = path.resolve(this.root, this.path, this.manifest.thumbnail);
      if (fs.exists(thumb)) {
        this.thumbnail = thumb;
      }
    }

    const range = this.manifest.engines && this.manifest.engines.arangodb;
    this.legacy = range ? semver.gtr('3.0.0', range) : false;
    if (this.legacy) {
      console.debug(
        `Running ${this.mount} in 2.x compatibility mode (requested version ${range} pre-dates 3.0.0)`
      );
    }

    this._reset();
  }

  applyConfiguration(config) {
    const definitions = this.manifest.configuration;
    const warnings = [];

    _.each(config, function (rawValue, name) {
      const def = definitions[name];
      if (!def) {
        warnings.push(`Unexpected option "${name}"`);
        return;
      }

      if (def.required === false && (rawValue === undefined || rawValue === null || rawValue === '')) {
        delete this.options.configuration[name];
        this.configuration[name] = def.default;
        return;
      }

      const validate = parameterTypes[def.type];
      let parsedValue = rawValue;
      let warning;

      if (validate.isJoi) {
        const result = validate.required().validate(rawValue);
        if (result.error) {
          warning = result.error.message.replace(/^"value"/, `"${name}"`);
        } else {
          parsedValue = result.value;
        }
      } else {
        try {
          parsedValue = validate(rawValue);
        } catch (e) {
          warning = `"${name}": ${e.message}`;
        }
      }

      if (warning) {
        warnings.push(warning);
      } else {
        this.options.configuration[name] = rawValue;
        this.configuration[name] = parsedValue;
      }
    }, this);

    return warnings;
  }

  buildRoutes() {
    const service = this;
    const tree = new Tree(this.main.context, this.router);
    this.docs = {
      swagger: '2.0',
      basePath: this.main.context.baseUrl,
      paths: tree.buildSwaggerPaths(),
      info: {
        title: this.name,
        description: this.manifest.description,
        version: this.version,
        license: {
          name: this.manifest.license
        }
      }
    };
    this.routes.routes.push({
      url: {match: '/*'},
      action: {
        callback(req, res, opts, next) {
          let handled = true;

          try {
            handled = tree.dispatch(req, res);
          } catch (e) {
            let error = e;
            if (!e.statusCode) {
              console.errorLines(e.stack);
              error = new InternalServerError();
              error.cause = e;
            }
            const body = {
              error: true,
              errorNum: error.statusCode,
              errorMessage: error.message,
              code: error.statusCode
            };
            if (error.statusCode === 405 && error.methods) {
              res.headers.allow = error.methods.join(', ');
            }
            if (service.isDevelopment) {
              let err = error.cause || error;
              body.exception = String(err);
              body.stacktrace = err.stack;
            }
            if (error.extra) {
              Object.keys(error.extra).forEach(function (key) {
                body[key] = error.extra[key];
              });
            }
            res.responseCode = error.statusCode;
            res.contentType = 'application/json';
            res.body = JSON.stringify(body);
          }

          if (!handled) {
            next();
          }
        }
      }
    });
  }

  applyDependencies(deps) {
    const definitions = this.manifest.dependencies;
    const warnings = [];

    _.each(deps, function (mount, name) {
      const dfn = definitions[name];
      if (!dfn) {
        warnings.push(`Unexpected dependency "${name}"`);
      } else {
        this.options.dependencies[name] = mount;
      }
    }, this);

    return warnings;
  }

  _PRINT(context) {
    context.output += `[FoxxService "${this.name}" (${this.version}) on ${this.mount}]`;
  }

  toJSON() {
    const result = {
      name: this.name,
      version: this.version,
      manifest: this.manifest,
      path: this.path,
      options: this.options,
      mount: this.mount,
      root: this.root,
      isSystem: this.isSystem,
      isDevelopment: this.isDevelopment
    };
    if (this.error) {
      result.error = this.error;
    }
    if (this.manifest.author) {
      result.author = this.manifest.author;
    }
    if (this.manifest.description) {
      result.description = this.manifest.description;
    }
    if (this.thumbnail) {
      try {
        result.thumbnail = fs.read64(this.thumbnail);
      } catch (e) {
        // noop
      }
    }
    return result;
  }

  simpleJSON() {
    return {
      name: this.name,
      version: this.version,
      mount: this.mount
    };
  }

  development(isDevelopment) {
    this.isDevelopment = isDevelopment;
  }

  getConfiguration(simple) {
    const config = {};
    const definitions = this.manifest.configuration;
    const options = this.options.configuration;
    _.each(definitions, function (dfn, name) {
      const value = options[name] === undefined ? dfn.default : options[name];
      config[name] = simple ? value : _.extend({}, dfn, {
        title: getReadableName(name),
        current: value
      });
    });
    return config;
  }

  getDependencies(simple) {
    const deps = {};
    const definitions = this.manifest.dependencies;
    const options = this.options.dependencies;
    _.each(definitions, function (dfn, name) {
      deps[name] = simple ? options[name] : {
        definition: dfn,
        title: getReadableName(name),
        current: options[name]
      };
    });
    return deps;
  }

  needsConfiguration() {
    const config = this.getConfiguration();
    const deps = this.getDependencies();
    return _.any(config, function (cfg) {
      return cfg.current === undefined && cfg.required !== false;
    }) || _.any(deps, function (dep) {
      return dep.current === undefined && dep.definition.required !== false;
    });
  }

  run(filename, options) {
    options = options || {};
    filename = path.resolve(this.main[$_MODULE_CONTEXT].__dirname, filename);

    const module = new Module(filename, this.main);
    module[$_MODULE_CONTEXT].console = this.main[$_MODULE_CONTEXT].console;
    module.context = _.extend(
      new FoxxContext(this),
      this.main.context,
      options.foxxContext
    );

    if (options.context) {
      Object.keys(options.context).forEach(function (key) {
        module[$_MODULE_CONTEXT][key] = options.context[key];
      });
    }

    if (this.legacy) {
      module[$_MODULE_CONTEXT].applicationContext = module.context;
    }

    module.load(filename);
    return module.exports;
  }

  _reset() {
    this.requireCache = {};
    const lib = this.manifest.lib || '.';
    const moduleRoot = path.resolve(this.root, this.path, lib);
    const foxxConsole = require('@arangodb/foxx/console')(this.mount);
    this.main = new Module(`foxx:${this.mount}`);
    this.main.filename = path.resolve(moduleRoot, '.foxx');
    this.main[$_MODULE_ROOT] = moduleRoot;
    this.main[$_MODULE_CONTEXT].console = foxxConsole;
    this.main.require.cache = this.requireCache;
    this.main.context = new FoxxContext(this);
    this.router = new Router();
    this.types = new Map(defaultTypes);
    this.routes = {
      name: `foxx("${this.mount}")`,
      routes: []
    };

    if (this.legacy) {
      this.main.context.foxxFilename = this.main.context.fileName;
    }
  }

  get exports() {
    return this.main.exports;
  }

  get name() {
    return this.manifest.name;
  }

  get version() {
    return this.manifest.version;
  }

  get isSystem() {
    return this.mount.charAt(1) === '_';
  }

  get root() {
    if (this.isSystem) {
      return FoxxService._systemAppPath;
    }
    return FoxxService._appPath;
  }

  get collectionPrefix() {
    return this.mount.substr(1).replace(/[-.:/]/g, '_') + '_';
  }

  static get _startupPath() {
    return STARTUP_PATH;
  }

  static get _appPath() {
    return APP_PATH ? (
      path.join(APP_PATH, '_db', internal.db._name())
    ) : undefined;
  }

  static get _systemAppPath() {
    return APP_PATH ? (
      path.join(STARTUP_PATH, 'apps', 'system')
    ) : undefined;
  }

  static get _oldAppPath() {
    return APP_PATH ? (
      path.join(APP_PATH, 'databases', internal.db._name())
    ) : undefined;
  }

  static get _devAppPath() {
    return DEV_APP_PATH ? (
      path.join(DEV_APP_PATH, 'databases', internal.db._name())
    ) : undefined;
  }
};

function createConfiguration(definitions) {
  const config = {};
  Object.keys(definitions).forEach(function (name) {
    const def = definitions[name];
    if (def.default !== undefined) {
      config[name] = def.default;
    }
  });
  return config;
}

function createDependencies(definitions, options) {
  const deps = {};
  Object.keys(definitions).forEach(function (name) {
    Object.defineProperty(deps, name, {
      configurable: true,
      enumerable: true,
      get() {
        const mount = options[name];
        return mount ? require('@arangodb/foxx').getExports(mount) : null;
      }
    });
  });
  return deps;
}
<|endoftext|>