<commit_before>/**
 * Copyright (c) 2009
 * Jan-Felix Schwarz
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **/

// Declare the MOVI.widget namespace
MOVI.namespace("widget");

(function() {

	var _SCROLLBOX_CLASS_NAME = "movi-scrollbox",
		_MODELIMG_CLASS_NAME = "movi-modelimg";

	/**
     * Array to keep track of all instances
	 * @property _instances
     * @private
     */
	var _instances = new Array();

    /**
     * A widget to display an Oryx model
     * @namespace MOVI.widget
     * @class MOVI.widget.ModelViewer
     * @extends YAHOO.util.Element
     * @constructor
     * @param {HTMLElement | String} el The id of the container DIV element that will
 	 * wrap the ModelViewer, or a reference to a DIV element. The DIV element must
	 * exist in the document.
     */
    MOVI.widget.ModelViewer = function(el) {

		// register the new instance
		this._index = _instances.length;
		_instances[this._index] = this;

    	MOVI.widget.ModelViewer.superclass.constructor.call(this, el);

		this.set("innerHTML", "");

	  	// create scrollbox element
		this._scrollbox = new YAHOO.util.Element(document.createElement("div"));
		this._scrollbox.set("className", _SCROLLBOX_CLASS_NAME);
		this._scrollbox.set("id", _SCROLLBOX_CLASS_NAME + this._index);
		this._scrollbox.set("innerHTML", "");
		this.appendChild(this._scrollbox);

		this._image = new YAHOO.util.Element(_MODELIMG_CLASS_NAME + this._index);
    };

	/**
     * Returns the ModelViewer instance with the specified id.
     * @method getInstance
     * @param {Integer | String} id The id of the ModelViewer instance,
	 * or the id of the instance's host DIV element.
     * @return {MOVI.widget.ModelViewer} A ModelViewer instance
     */
	MOVI.widget.ModelViewer.getInstance = function(id) {
		if(YAHOO.lang.isNumber(id)) {
			return _instances[id];
		} else {
			for(key in _instances) {
				if(_instances[key].get("id")==id)
					return _instances[key];
			}
		}
	};

 	MOVI.extend(MOVI.widget.ModelViewer, YAHOO.util.Element, {

		/**
	     * URI of the model to display
	     * @property _modelUri
		 * @type String
		 * @private
	     */
		_modelUri: "",

		/**
	     * Options passed to loadModel method
	     * @property _loadOptions
		 * @type Object
		 * @private
	     */
		_loadOptions: {},

		/**
	     * The canvas container Element realizing scrolling
	     * @property _scrollbox
		 * @type Element
		 * @private
	     */
		_scrollbox: null,

		/**
	     * The image element
	     * @property _image
		 * @type Element
		 * @private
	     */
		_image: null,

		/**
		 * The model's canvas
		 * @property canvas
		 * @type Canvas
		 */
		canvas: {},

		/**
	     * Callback that is executed when the model is finished
		 * loading.
	     * @method _onSuccess
		 * @private
	     */
		_onSuccess: function() {
			MOVI.log("Model loaded successfully.", "info", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onSuccess)
				this._loadOptions.onSuccess.call(scope, this);
		},

		/**
	     * Callback for handling model load failures.
	     * @method _onLoadFailure
		 * @private
	     */
		_onLoadFailure: function() {
			MOVI.log("Could not load model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling model load timeouts.
	     * @method _onLoadTimeout
		 * @private
	     */
		_onLoadTimeout: function() {
			MOVI.log("A timeout occured while trying to load model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onTimeout)
				this._loadOptions.onTimeout.call(scope, this);
			else if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling stencil set load failures.
	     * @method _onStencilSetLoadFailure
		 * @private
	     */
		_onStencilSetLoadFailure: function() {
			MOVI.log("Could not load stencil set for model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling stencil set load timeouts.
	     * @method _onStencilSetLoadTimeout
		 * @private
	     */
		_onStencilSetLoadTimeout: function() {
			MOVI.log("A timeout occured while trying to load stencil set for model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onTimeout)
				this._loadOptions.onTimeout.call(scope, this);
			else if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Loads the specified Oryx model in the viewer.
	     * @method loadModel
	     * @param {String} uri The URI of the model (without /self).
		 * For example: 'http://oryx-editor.org/backend/poem/model/1234'
		 * @param {Object} opt Options:
         *

         *
onSuccess

         *

         * Callback to execute when the model is finished loading
         * The callback receives the ModelViewer instance back.
         *

         *
onFailure

         *

         * Callback to execute when the model load operation fails
         * The callback receives the ModelViewer instance back.
         *

		 *
onTimeout

         *

         * Callback to execute when a timeout occurs. If not set
		 * the onFailure callback will be executed on timeout.
         * The callback receives the ModelViewer instance back.
         *

 		 *
scope

		 *
The execution scope for the callbacks.

 	 	 *
timeout

         *

         * Number of milliseconds to wait before aborting the loading
		 * of the model and executing the onFailure callback.
		 * The default value is 15000 (15 seconds).
         *

		 *

		 * @throws Exception, if passed URI is not valid
	     */
		loadModel: function(uri, opt) {

			if(!YAHOO.lang.isString(uri)) {
				throw new URIError("No valid URI passed to loadModel." , "modelviewer.js");
				this._onLoadFailure();
			}

			this._modelUri = uri;

			this._loadOptions = opt || {};
			if(!this._loadOptions.timeout)
				this._loadOptions.timeout = 15000; // default timeout

			var jsonp = encodeURIComponent(
				"MOVI.widget.ModelViewer.getInstance(" +
				this._index + ").loadModelCallback");
			var url = uri + "/json?jsonp=" + jsonp;

			var transactionObj = YAHOO.util.Get.script(url, {
				onFailure: this._onLoadFailure,
				onTimeout: this._onLoadTimeout,
				timeout  : this._loadOptions.timeout,
				scope    : this
			});

			this._loadImage(uri);
		},

		/**
	     * Load the PNG image representation of the model.
	     * @method _loadImage
		 * @param {String} uri The URI of the model (without /self).
		 * @private
	     */
		_loadImage: function(uri) {
			this._image.set("src", uri + "/png");
		},

		/**
	     * JSONP callback to load model data from the JSON web service
	     * @method loadModelCallback
	     * @param jsonObj The delivered JSON Object
	     */
		loadModelCallback: function(jsonObj) {

			this.canvas = jsonObj;

			if(!this.canvas.stencilset) {
				MOVI.log("Could not find stencilset definition for model.", "error", "canvas.js");
				this._onFailure();
			}

			var jsonp = encodeURIComponent(
				"MOVI.widget.ModelViewer.getInstance(" +
				this._index + ").loadStencilSetCallback");
			var i = this.canvas.stencilset.url.indexOf("/stencilsets/");
			var url = this.canvas.stencilset.url.substring(0, i) +
				"/jsonp?resource=" + encodeURIComponent(this.canvas.stencilset.url.substring(i+13)) +
				"&jsonp=" + jsonp;

			var transactionObj = YAHOO.util.Get.script(url, {
				onFailure: this._onStencilSetLoadFailure,
				onTimeout: this._onStencilSetLoadTimeout,
				data	 : this._loadOptions,
				timeout  : this._loadOptions.timeout,
				scope    : this
			});

		},

		/**
	     * JSONP callback to load stencilset data from the JSON web service
	     * @method loadStencilSetCallback
	     * @param jsonObj The delivered JSON Object
	     */
		loadStencilSetCallback: function(jsonObj) {
			try {
				this.canvas.stencilset = new MOVI.stencilset.Stencilset(jsonObj);
				this.canvas = new MOVI.model.Canvas(this, this.canvas);
				this._scrollbox.appendChild(this.canvas);
				this._onSuccess();
			} catch(e) {
				MOVI.log("A " + e.name + " occured while trying to load model: " +
							e.message, "error", "modelviewer.js");
				this._onLoadFailure();
			}

		},

		/**
		 * Returns the width of the model image element in pixels
		 * @method getImgWidth
		 * @return {Integer} The image width
		 */
		getImgWidth: function() {
			return this._image.get("width");
		},

		/**
		 * Returns the height of the model image element in pixels
		 * @method getImgHeight
		 * @return {Integer} The image height
		 */
		getImgHeight: function() {
			return this._image.get("height");
		},

		/**
		 * Returns the absolute URI of the displayed model
		 * @method getImgHeight
		 * @return {String} The models absolute URI
		 */
		getModelUri: function() {
			return this._modelUri;
		},

		/**
		 * Returns the canvas container Element realizing scrolling
		 * @method getScrollboxEx
		 * @return {Element} The scrollbox Element
		 */
		getScrollboxEl: function() {
			return this._scrollbox;
		},

		/**
		 * Scrolls to the shape, i.e. centers the (canvas around the) shape
		 * within the scrollbox.
		 * @method scrollToShape()
		 * @param shape the shape or its resource id
		 * @return {Element} The shape element / null if it does not exist
		 * @throws Error if scrollbox dimensions cannot be calculated
		 */
		scrollToShape: function(shape) {
			if ("string" == typeof(shape)) {
				shape = this.canvas.getShape(shape);
				if (null == shape) {
					return null;
				}
			}

			if (null != (h = this.getScrollboxEl().getStyle("height").match(/(\d+)px/)) &&
			    null != (w = this.getScrollboxEl().getStyle("width").match(/(\d+)px/)))
			{
				var bounds = shape.getAbsBounds();

				var origin = {
					x: bounds.upperLeft.x + (bounds.lowerRight.x - bounds.upperLeft.x)/2,
					y: bounds.upperLeft.y + (bounds.lowerRight.y - bounds.upperLeft.y)/2
				}

				var target = {
					x: parseInt(origin.x - parseInt(w[1])/2 ),
					y: parseInt(origin.y - parseInt(h[1])/2 )
				}

				this.getScrollboxEl().set("scrollTop", target.y);
				this.getScrollboxEl().set("scrollLeft", target.x);
			}
			else {
				throw new Error("Unable to calculate scrollbox dimensions", "modelviewer.js");
			}

			return shape;
		}

	});

})();<commit_msg>MOVI API: made reloading of model img possible<commit_after>/**
 * Copyright (c) 2009
 * Jan-Felix Schwarz
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 **/

// Declare the MOVI.widget namespace
MOVI.namespace("widget");

(function() {

	var _SCROLLBOX_CLASS_NAME = "movi-scrollbox",
		_MODELIMG_CLASS_NAME = "movi-modelimg";

	/**
     * Array to keep track of all instances
	 * @property _instances
     * @private
     */
	var _instances = new Array();

    /**
     * A widget to display an Oryx model
     * @namespace MOVI.widget
     * @class MOVI.widget.ModelViewer
     * @extends YAHOO.util.Element
     * @constructor
     * @param {HTMLElement | String} el The id of the container DIV element that will
 	 * wrap the ModelViewer, or a reference to a DIV element. The DIV element must
	 * exist in the document.
     */
    MOVI.widget.ModelViewer = function(el) {

		// register the new instance
		this._index = _instances.length;
		_instances[this._index] = this;

    	MOVI.widget.ModelViewer.superclass.constructor.call(this, el);

		this.set("innerHTML", "");

	  	// create scrollbox element
		this._scrollbox = new YAHOO.util.Element(document.createElement("div"));
		this._scrollbox.set("className", _SCROLLBOX_CLASS_NAME);
		this._scrollbox.set("id", _SCROLLBOX_CLASS_NAME + this._index);
		this._scrollbox.set("innerHTML", "");
		this.appendChild(this._scrollbox);

		this._image = new YAHOO.util.Element(_MODELIMG_CLASS_NAME + this._index);
    };

	/**
     * Returns the ModelViewer instance with the specified id.
     * @method getInstance
     * @param {Integer | String} id The id of the ModelViewer instance,
	 * or the id of the instance's host DIV element.
     * @return {MOVI.widget.ModelViewer} A ModelViewer instance
     */
	MOVI.widget.ModelViewer.getInstance = function(id) {
		if(YAHOO.lang.isNumber(id)) {
			return _instances[id];
		} else {
			for(key in _instances) {
				if(_instances[key].get("id")==id)
					return _instances[key];
			}
		}
	};

 	MOVI.extend(MOVI.widget.ModelViewer, YAHOO.util.Element, {

		/**
	     * URI of the model to display
	     * @property _modelUri
		 * @type String
		 * @private
	     */
		_modelUri: "",

		/**
	     * Options passed to loadModel method
	     * @property _loadOptions
		 * @type Object
		 * @private
	     */
		_loadOptions: {},

		/**
	     * The canvas container Element realizing scrolling
	     * @property _scrollbox
		 * @type Element
		 * @private
	     */
		_scrollbox: null,

		/**
	     * The image element
	     * @property _image
		 * @type Element
		 * @private
	     */
		_image: null,

		/**
		 * The model's canvas
		 * @property canvas
		 * @type Canvas
		 */
		canvas: {},

		/**
	     * Callback that is executed when the model is finished
		 * loading.
	     * @method _onSuccess
		 * @private
	     */
		_onSuccess: function() {
			MOVI.log("Model loaded successfully.", "info", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onSuccess)
				this._loadOptions.onSuccess.call(scope, this);
		},

		/**
	     * Callback for handling model load failures.
	     * @method _onLoadFailure
		 * @private
	     */
		_onLoadFailure: function() {
			MOVI.log("Could not load model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling model load timeouts.
	     * @method _onLoadTimeout
		 * @private
	     */
		_onLoadTimeout: function() {
			MOVI.log("A timeout occured while trying to load model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onTimeout)
				this._loadOptions.onTimeout.call(scope, this);
			else if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling stencil set load failures.
	     * @method _onStencilSetLoadFailure
		 * @private
	     */
		_onStencilSetLoadFailure: function() {
			MOVI.log("Could not load stencil set for model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Callback for handling stencil set load timeouts.
	     * @method _onStencilSetLoadTimeout
		 * @private
	     */
		_onStencilSetLoadTimeout: function() {
			MOVI.log("A timeout occured while trying to load stencil set for model.", "error", "modelviewer.js");
			var scope = this._loadOptions.scope || window;
			if(this._loadOptions.onTimeout)
				this._loadOptions.onTimeout.call(scope, this);
			else if(this._loadOptions.onFailure)
				this._loadOptions.onFailure.call(scope, this);
		},

		/**
	     * Loads the specified Oryx model in the viewer.
	     * @method loadModel
	     * @param {String} uri The URI of the model (without /self).
		 * For example: 'http://oryx-editor.org/backend/poem/model/1234'
		 * @param {Object} opt Options:
         *

         *
onSuccess

         *

         * Callback to execute when the model is finished loading
         * The callback receives the ModelViewer instance back.
         *

         *
onFailure

         *

         * Callback to execute when the model load operation fails
         * The callback receives the ModelViewer instance back.
         *

		 *
onTimeout

         *

         * Callback to execute when a timeout occurs. If not set
		 * the onFailure callback will be executed on timeout.
         * The callback receives the ModelViewer instance back.
         *

 		 *
scope

		 *
The execution scope for the callbacks.

 	 	 *
timeout

         *

         * Number of milliseconds to wait before aborting the loading
		 * of the model and executing the onFailure callback.
		 * The default value is 15000 (15 seconds).
         *

		 *

		 * @throws Exception, if passed URI is not valid
	     */
		loadModel: function(uri, opt) {

			if(!YAHOO.lang.isString(uri)) {
				throw new URIError("No valid URI passed to loadModel." , "modelviewer.js");
				this._onLoadFailure();
			}

			this._modelUri = uri;

			this._loadOptions = opt || {};
			if(!this._loadOptions.timeout)
				this._loadOptions.timeout = 15000; // default timeout

			var jsonp = encodeURIComponent(
				"MOVI.widget.ModelViewer.getInstance(" +
				this._index + ").loadModelCallback");
			var url = uri + "/json?jsonp=" + jsonp;

			var transactionObj = YAHOO.util.Get.script(url, {
				onFailure: this._onLoadFailure,
				onTimeout: this._onLoadTimeout,
				timeout  : this._loadOptions.timeout,
				scope    : this
			});

			this._loadImage(uri);
		},

		/**
	     * Load the PNG image representation of the model.
	     * @method _loadImage
		 * @param {String} uri The URI of the model (without /self).
		 * @private
	     */
		_loadImage: function(uri) {
			// append timestamp to allow reloads of image
			this._image.set("src", uri + "/png?" + (new Date()).getTime());
		},

		/**
	     * JSONP callback to load model data from the JSON web service
	     * @method loadModelCallback
	     * @param jsonObj The delivered JSON Object
	     */
		loadModelCallback: function(jsonObj) {

			this.canvas = jsonObj;

			if(!this.canvas.stencilset) {
				MOVI.log("Could not find stencilset definition for model.", "error", "canvas.js");
				this._onFailure();
			}

			var jsonp = encodeURIComponent(
				"MOVI.widget.ModelViewer.getInstance(" +
				this._index + ").loadStencilSetCallback");
			var i = this.canvas.stencilset.url.indexOf("/stencilsets/");
			var url = this.canvas.stencilset.url.substring(0, i) +
				"/jsonp?resource=" + encodeURIComponent(this.canvas.stencilset.url.substring(i+13)) +
				"&jsonp=" + jsonp;

			var transactionObj = YAHOO.util.Get.script(url, {
				onFailure: this._onStencilSetLoadFailure,
				onTimeout: this._onStencilSetLoadTimeout,
				data	 : this._loadOptions,
				timeout  : this._loadOptions.timeout,
				scope    : this
			});

		},

		/**
	     * JSONP callback to load stencilset data from the JSON web service
	     * @method loadStencilSetCallback
	     * @param jsonObj The delivered JSON Object
	     */
		loadStencilSetCallback: function(jsonObj) {
			try {
				this.canvas.stencilset = new MOVI.stencilset.Stencilset(jsonObj);
				this.canvas = new MOVI.model.Canvas(this, this.canvas);
				this._scrollbox.appendChild(this.canvas);
				this._onSuccess();
			} catch(e) {
				MOVI.log("A " + e.name + " occured while trying to load model: " +
							e.message, "error", "modelviewer.js");
				this._onLoadFailure();
			}

		},

		/**
		 * Returns the width of the model image element in pixels
		 * @method getImgWidth
		 * @return {Integer} The image width
		 */
		getImgWidth: function() {
			return this._image.get("width");
		},

		/**
		 * Returns the height of the model image element in pixels
		 * @method getImgHeight
		 * @return {Integer} The image height
		 */
		getImgHeight: function() {
			return this._image.get("height");
		},

		/**
		 * Returns the absolute URI of the displayed model
		 * @method getImgHeight
		 * @return {String} The models absolute URI
		 */
		getModelUri: function() {
			return this._modelUri;
		},

		/**
		 * Returns the canvas container Element realizing scrolling
		 * @method getScrollboxEx
		 * @return {Element} The scrollbox Element
		 */
		getScrollboxEl: function() {
			return this._scrollbox;
		},

		/**
		 * Scrolls to the shape, i.e. centers the (canvas around the) shape
		 * within the scrollbox.
		 * @method scrollToShape()
		 * @param shape the shape or its resource id
		 * @return {Element} The shape element / null if it does not exist
		 * @throws Error if scrollbox dimensions cannot be calculated
		 */
		scrollToShape: function(shape) {
			if ("string" == typeof(shape)) {
				shape = this.canvas.getShape(shape);
				if (null == shape) {
					return null;
				}
			}

			if (null != (h = this.getScrollboxEl().getStyle("height").match(/(\d+)px/)) &&
			    null != (w = this.getScrollboxEl().getStyle("width").match(/(\d+)px/)))
			{
				var bounds = shape.getAbsBounds();

				var origin = {
					x: bounds.upperLeft.x + (bounds.lowerRight.x - bounds.upperLeft.x)/2,
					y: bounds.upperLeft.y + (bounds.lowerRight.y - bounds.upperLeft.y)/2
				}

				var target = {
					x: parseInt(origin.x - parseInt(w[1])/2 ),
					y: parseInt(origin.y - parseInt(h[1])/2 )
				}

				this.getScrollboxEl().set("scrollTop", target.y);
				this.getScrollboxEl().set("scrollLeft", target.x);
			}
			else {
				throw new Error("Unable to calculate scrollbox dimensions", "modelviewer.js");
			}

			return shape;
		}

	});

})();<|endoftext|>


<commit_before>/*
 * grunt-init-amber
 * https://amber-lang.net/
 *
 * Copyright (c) 2013 Manfred Kroehnert, contributors
 * Licensed under the MIT license.
 */

'use strict';

// Basic template description.
exports.description = 'Create a web application based on Amber Smalltalk.';

// Template-specific notes to be displayed before question prompts.
exports.notes = ' _Project title_ should be a human-readable title.';

// Template-specific notes to be displayed after question prompts.
exports.after = 'You should now install project dependencies with _bower install_.' +
  ' Afterwards, start the development server with _./bower_components/amber/bin/amber serve_.' +
  ' Your application is then accessible via _http://localhost:4000/_';

// Any existing file or directory matching this wildcard will cause a warning.
exports.warnOn = '*';

// The actual init template.
exports.template = function(grunt, init, done) {
  var remembered = {};
  function rememberViaValidator(name) {
    var oldValidator = init.prompts[name].validator || function (line) {
      return true;
    };

    var newValidator;
    switch (oldValidator.length) { //apply would not work, .length is used to call it differently
      case 1:
        newValidator = function (line) {
          remembered[name] = line;
          return oldValidator.call(this, line);
        };
        break;
      case 2:
        newValidator = function (line, next) {
          remembered[name] = line;
          return oldValidator.call(this, line, next);
        };
        break;
      default:
        throw new Error("Panic: " + oldValidator.length + "-argument validator for " + name + ".");
    }

    init.prompts[name].validator = newValidator;
  }

  function capitalize(string) {
    return string[0].toUpperCase() + string.slice(1).toLowerCase();
  }

  init.prompts.name.message= 'Main class and package of Amber application.\nProject name is derived by lowercasing this.';
  init.prompts.name.validator= function (line) { return /^[A-Z][A-Za-z0-9]*$/.test(line) };
  init.prompts.name.warning= 'Must be a valid class name: only alphanumeric and starting with an uppercase letter!';
  rememberViaValidator('name');
  rememberViaValidator('title');

  init.process({type: 'amber'}, [
    // Prompt for these values.
    init.prompt('title', 'Application or Library Title'),
    init.prompt('name', function (value, data, done) {
      var words = remembered.title.split(/\W/);
      words = words.filter(function (x) {
        return x && x !== "none";
      }).map(capitalize);
      value = words.length ? words.join('') : 'MysteriousApp';
      done(null, value);
    }),
    init.prompt('description', 'The Application or The Library doing The Thing.'),
    init.prompt('author_name'),
    init.prompt('author_email'),
    {
      name: 'namespace',
      message: 'Namespace of the new Amber package.',
      altDefault: function(value, data, done) {
        value = 'amber-' + remembered.name.toLowerCase();
        done(null, value);
      },
      validator: /^[a-z][a-z0-9/\-]*$/,
      warning: 'Only lowercase letters, numbers, and - are allowed in namespaces!'
    },
    init.prompt('version'),
    init.prompt('repository'),
    init.prompt('homepage'),
    init.prompt('bugs'),
    init.prompt('author_url'),
    init.prompt('licenses', 'MIT')
  ], function(err, props) {
    // Files to copy (and process).
    var files = init.filesToCopy(props);

    // Add properly-named license files.
    init.addLicenseFiles(files, props.licenses);

    // Actually copy (and process) files.
    init.copyAndProcess(files, props, {noProcess: 'libs/**'});

    // Clean up non-npm props.
    delete props.namespace;

    props.name = props.name.toLowerCase();

    // A few additional properties.
    props.keywords = ['Amber', 'Smalltalk'];
    props.devDependencies = {
      "grunt": "~0.4.0",
      "grunt-execute": "~0.2.1",
      "grunt-contrib-clean": "~0.5.0",
      "amber-dev": "~0.1.1"
    };
    props.node_version = '>= 0.8.0';
    props.scripts = {
      "test": "grunt test"
    };

    // Generate package.json file, used by npm and grunt.
    init.writePackageJSON('package.json', props);

    // generate bower.json file
    grunt.file.write('bower.json', JSON.stringify({

        "name": props.name,
        "description": props.description,
        "version": props.version,
        "ignore": [
          "**/.*",
          "node_modules",
          "bower_components",
          "/test_runner.js",
          "test",
          "tests"
        ],
        "authors": [
            {
                "name": props.author_name,
                "email": props.author_email
            }
        ],
        "homepage": props.homepage,
        "main": props.main,
        "keywords": props.keywords,
        "license": props.licenses,
        "private": false,
        "dependencies": {
            "amber": "~0.12.4"
        }
    }, null, 4));

    // All done!
    done();
  });

};
<commit_msg>Changed the after text.<commit_after>/*
 * grunt-init-amber
 * https://amber-lang.net/
 *
 * Copyright (c) 2013 Manfred Kroehnert, contributors
 * Licensed under the MIT license.
 */

'use strict';

// Basic template description.
exports.description = 'Create a web application based on Amber Smalltalk.';

// Template-specific notes to be displayed before question prompts.
exports.notes = ' _Project title_ should be a human-readable title.';

// Template-specific notes to be displayed after question prompts.
exports.after = 'You need to have these installed globally via npm:' +
  ' _amber-cli_; _grunt-cli_; _bower_.' +
  ' Now, install project dependencies with _bower install_,' +
  ' tool dependencies with _npm install_ and optionally, recompile with _grunt_.' +
  ' If you are running _amber init_, these three tasks are going to be performed for you now.' +
  ' Afterwards, start the development server with _amber serve_.' +
  ' Your application is then accessible via _http://localhost:4000/_';

// Any existing file or directory matching this wildcard will cause a warning.
exports.warnOn = '*';

// The actual init template.
exports.template = function(grunt, init, done) {
  var remembered = {};
  function rememberViaValidator(name) {
    var oldValidator = init.prompts[name].validator || function (line) {
      return true;
    };

    var newValidator;
    switch (oldValidator.length) { //apply would not work, .length is used to call it differently
      case 1:
        newValidator = function (line) {
          remembered[name] = line;
          return oldValidator.call(this, line);
        };
        break;
      case 2:
        newValidator = function (line, next) {
          remembered[name] = line;
          return oldValidator.call(this, line, next);
        };
        break;
      default:
        throw new Error("Panic: " + oldValidator.length + "-argument validator for " + name + ".");
    }

    init.prompts[name].validator = newValidator;
  }

  function capitalize(string) {
    return string[0].toUpperCase() + string.slice(1).toLowerCase();
  }

  init.prompts.name.message= 'Main class and package of Amber application.\nProject name is derived by lowercasing this.';
  init.prompts.name.validator= function (line) { return /^[A-Z][A-Za-z0-9]*$/.test(line) };
  init.prompts.name.warning= 'Must be a valid class name: only alphanumeric and starting with an uppercase letter!';
  rememberViaValidator('name');
  rememberViaValidator('title');

  init.process({type: 'amber'}, [
    // Prompt for these values.
    init.prompt('title', 'Application or Library Title'),
    init.prompt('name', function (value, data, done) {
      var words = remembered.title.split(/\W/);
      words = words.filter(function (x) {
        return x && x !== "none";
      }).map(capitalize);
      value = words.length ? words.join('') : 'MysteriousApp';
      done(null, value);
    }),
    init.prompt('description', 'The Application or The Library doing The Thing.'),
    init.prompt('author_name'),
    init.prompt('author_email'),
    {
      name: 'namespace',
      message: 'Namespace of the new Amber package.',
      altDefault: function(value, data, done) {
        value = 'amber-' + remembered.name.toLowerCase();
        done(null, value);
      },
      validator: /^[a-z][a-z0-9/\-]*$/,
      warning: 'Only lowercase letters, numbers, and - are allowed in namespaces!'
    },
    init.prompt('version'),
    init.prompt('repository'),
    init.prompt('homepage'),
    init.prompt('bugs'),
    init.prompt('author_url'),
    init.prompt('licenses', 'MIT')
  ], function(err, props) {
    // Files to copy (and process).
    var files = init.filesToCopy(props);

    // Add properly-named license files.
    init.addLicenseFiles(files, props.licenses);

    // Actually copy (and process) files.
    init.copyAndProcess(files, props, {noProcess: 'libs/**'});

    // Clean up non-npm props.
    delete props.namespace;

    props.name = props.name.toLowerCase();

    // A few additional properties.
    props.keywords = ['Amber', 'Smalltalk'];
    props.devDependencies = {
      "grunt": "~0.4.0",
      "grunt-execute": "~0.2.1",
      "grunt-contrib-clean": "~0.5.0",
      "amber-dev": "~0.1.1"
    };
    props.node_version = '>= 0.8.0';
    props.scripts = {
      "test": "grunt test"
    };

    // Generate package.json file, used by npm and grunt.
    init.writePackageJSON('package.json', props);

    // generate bower.json file
    grunt.file.write('bower.json', JSON.stringify({

        "name": props.name,
        "description": props.description,
        "version": props.version,
        "ignore": [
          "**/.*",
          "node_modules",
          "bower_components",
          "/test_runner.js",
          "test",
          "tests"
        ],
        "authors": [
            {
                "name": props.author_name,
                "email": props.author_email
            }
        ],
        "homepage": props.homepage,
        "main": props.main,
        "keywords": props.keywords,
        "license": props.licenses,
        "private": false,
        "dependencies": {
            "amber": "~0.12.4"
        }
    }, null, 4));

    // All done!
    done();
  });

};
<|endoftext|>


<commit_before>enyo.kind({
	name: "moon.sample.ContextualPopupSample",
	classes: "moon enyo-unselectable enyo-fit",
	components: [
		{
			kind: "moon.ContextualPopupDecorator",
			style:"position: absolute; left: 0px; top: 0px;",
			components:
			[
				{content: "Average"},
				{
					kind: "moon.ContextualPopup",
					classes: "moon-2h moon-8v",
					components: [
						{content:"Item 1"},
						{content:"Item 2"},
						{content:"Item 3"}
					]
				}
			]
		},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; right: 0px; top: 0px;", components: [
			{content:"Small", small:true},
			{kind: "moon.ContextualPopup"}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; right: 0px; top: 13%;", components: [
			{content:"Nested Radio", small:true},
				{kind: "moon.ContextualPopup", components:[
					{name:"nestedRadioGroup", kind: "moon.RadioItemGroup", components: [
						{content: "Creek", selected: true},
						{content: "River"},
						{content: "Ocean"}
					]},
					{components:[
						{content: "Radio Group Value"},
						{name: "nestedRadioValue"}
					]}
				]}
			]
		},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; left: 0px; top: 25%;", components: [
			{content: "Left"},
			{
				kind: "moon.ContextualPopup",
				classes: "moon-3h moon-4v",
				components: [
					{content:"Item 1"},
					{content:"Item 2"},
					{content:"Item 3"}
				]
			}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; right: 0px; top: 25%;", components: [
			{content:"Right", small:true},
			{kind: "moon.ContextualPopup", components: [
				{content:"Outside scroller", kind: "moon.Item"},
				{classes: "moon-8h moon-6v", components: [
					{kind: "moon.Scroller", classes: "enyo-fill", components: [
						{content:"testing 1", kind: "moon.Item"},
						{content:"testing 2", kind: "moon.Item"},
						{content:"testing 3", kind: "moon.Item"},
						{content:"testing 4", kind: "moon.Item"},
						{content:"testing 5", kind: "moon.Item"},
						{content:"testing 6", kind: "moon.Item"},
						{content:"testing 7", kind: "moon.Item"},
						{content:"testing 8", kind: "moon.Item"},
						{content:"testing 9", kind: "moon.Item"},
						{content:"testing 10", kind: "moon.Item"},
						{content:"testing 12", kind: "moon.Item"},
						{content:"testing 13", kind: "moon.Item"},
						{content:"testing 14", kind: "moon.Item"},
						{content:"testing 15", kind: "moon.Item"},
						{content:"testing 16", kind: "moon.Item"},
						{content:"testing 17", kind: "moon.Item"},
						{content:"testing 18", kind: "moon.Item"},
						{content:"testing 19", kind: "moon.Item"}
					]}

				]}
			]}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; left: 0px; top: 45%;", components: [
			{content: "Wide"},
			{kind: "moon.ContextualPopup", classes: "moon-6h moon-4v", components: [
				{kind: "moon.Scroller", classes: "enyo-fill", components:[
					{content:"testing 1"},
					{content:"testing 2"}
				]}
			]}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; right: 0px; top: 45%;", components: [
			{content:"Long but Small Button with truncation", small:true},
			{kind: "moon.ContextualPopup", components: [
				{content:"Outside scroller", kind: "moon.Item"},
				{classes: "moon-16v", components: [
					{kind: "moon.Scroller", classes: "enyo-fill", components: [
						{content:"testing 1", kind: "moon.Item"},
						{content:"testing 2", kind: "moon.Item"},
						{content:"testing 3", kind: "moon.Item"},
						{content:"testing 4", kind: "moon.Item"},
						{content:"testing 5", kind: "moon.Item"},
						{content:"testing 6", kind: "moon.Item"},
						{content:"testing 7", kind: "moon.Item"},
						{content:"testing 8", kind: "moon.Item"},
						{content:"testing 9", kind: "moon.Item"},
						{content:"testing 10", kind: "moon.Item"},
						{content:"testing 12", kind: "moon.Item"},
						{content:"testing 13", kind: "moon.Item"},
						{content:"testing 14", kind: "moon.Item"},
						{content:"testing 15", kind: "moon.Item"},
						{content:"testing 16", kind: "moon.Item"},
						{content:"testing 17", kind: "moon.Item"},
						{content:"testing 18", kind: "moon.Item"},
						{content:"testing 19", kind: "moon.Item"}
					]}

				]}
			]}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; left: 0px; top: 65%;", components: [
			{content: "Deactivated", disabled:true},
			{kind: "moon.ContextualPopup", classes: "moon-6h moon-4v", components: [
				{kind: "moon.Scroller", classes: "enyo-fill", components:[
					{content:"testing 1"},
					{content:"testing 2"}
				]}
			]}
		]},
		{kind: "moon.ContextualPopupDecorator", style:"position: absolute; right: 0px; top: 65%;", components: [
			{content: "Small Deactivated", small:true, disabled:true},
			{kind: "moon.ContextualPopup", classes: "moon-6h moon-4v", components: [
				{kind: "moon.Scroller", classes: "enyo-fill", components:[
					{content:"testing 1"},
					{content:"testing 2"}
				]}
			]}
		]},
		{kind: "moon.ContextualPopupDecorator", style: "position: absolute; left: 0px; bottom: 0px;", components: [
			{content: "Spotlight Modal"},
			{
				kind: "moon.ContextualPopup",
				name: "buttonPopup",
				classes: "moon-8h moon-8v",
				modal: true,
				autoDismiss: false,
				spotlightModal: true,
				components: [
					{kind: "moon.Scroller", horizontal: "auto", classes: "enyo-fill", components: [
						{kind: "moon.Button", content: "Button"},
						{kind: "moon.ToggleButton", content: "SpotlightModal", value: true, ontap: "buttonToggled"},
						{tag: "br"},
						{tag: "br"},
						{kind: "moon.InputDecorator", spotlight: true, components: [
							{kind: "moon.Input", placeholder: "USERNAME"}
						]}
					]}
				]
			}
		]},
		{kind: "moon.ContextualPopupDecorator", style: "position: absolute; right: 0px; bottom: 0px;", components: [
			{content: "Spottable", small:true},
			{
				kind: "moon.ContextualPopup",
				classes: "moon-9h moon-4v",
				components: [
					{kind: "moon.Scroller", horizontal: "auto", classes: "enyo-fill", components: [
						{kind: "moon.Button", content: "Button 1"},
						{kind: "moon.Button", content: "Button 2"},
						{kind: "moon.Button", content: "Button 3"}
					]}
				]
			}
		]},
		{style: 'position: absolute; left: 30%; top: 25%;', components: [
			{kind: 'moon.Divider', content: 'Button Position', classes: 'radioItemFont'},
			{kind: moon.InputDecorator, components: [
				{kind: 'moon.Input', style: 'width: 120px', name:'leftInput', placeholder: 'left (px or %)', classes: 'radioItemFont'}
			]},
			{kind: 'moon.InputDecorator', components: [
				{kind: 'moon.Input', style: 'width: 120px', name:'topInput', placeholder: 'top (px or %)', classes: 'radioItemFont'}
			]},
			{kind: 'moon.Button', small: true, content: 'Set Position', ontap: 'setPosition'}
		]},
		{style: 'position: absolute; left: 30%; top: 50%; ', components: [
			{kind: 'moon.Divider', content: 'Popup Direction', classes: 'radioItemFont'},
			{kind: 'moon.RadioItemGroup', onActivate: 'groupChanged', components: [
				{content: 'none', classes: 'radioItemFont'},
				{content: 'left', classes: 'radioItemFont'},
				{content: 'right', classes: 'radioItemFont'},
				{content: 'top', classes: 'radioItemFont'},
				{content: 'bottom', classes: 'radioItemFont'}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', name: 'directionButton', style: 'position: absolute; left: 40%; top: 70%;', components: [
			{content: 'Direction'},
			{
				kind: 'moon.ContextualPopup',
				name: 'directionContext',
				classes: 'moon-4v',
				components: [
					{kind: moon.Scroller, horizontal: 'auto', classes: 'enyo-fill', components: [
						{kind: moon.Button, content: 'Button 1'},
						{kind: moon.Button, content: 'Button 2'}
					]}
				]
			}
		]}
	],
	bindings: [
		{from: ".$.nestedRadioGroup.active.content", to: ".$.nestedRadioValue.content"}
	],
	buttonToggled: function(inSender, inEvent) {
		this.$.buttonPopup.setSpotlightModal(inSender.getActive());
		this.$.buttonPopup.setAutoDismiss(!inSender.getActive());
	},
	setPosition: function(){
		this.$.directionButton.applyStyle('left', this.$.leftInput.getValue() === '' ? '40%' : this.$.leftInput.getValue());
		this.$.directionButton.applyStyle('top', this.$.topInput.getValue() === '' ? '70%' : this.$.topInput.getValue());
	},
	groupChanged: function(inSender, inEvent) {
		if(inEvent.originator.getActive() && inEvent.originator.kind === 'moon.RadioItem') {
			var selected = inEvent.originator.getContent();
			this.$.directionContext.set('direction', selected);
		}
	}
});<commit_msg>added dismiss, and cleaned up quotes<commit_after>enyo.kind({
	name: 'moon.sample.ContextualPopupSample',
	classes: 'moon enyo-unselectable enyo-fit',
	components: [
		{
			kind: 'moon.ContextualPopupDecorator',
			style:'position: absolute; left: 0px; top: 0px;',
			components:
			[
				{content: 'Average'},
				{
					kind: 'moon.ContextualPopup',
					classes: 'moon-2h moon-8v',
					components: [
						{content:'Item 1'},
						{content:'Item 2'},
						{content:'Item 3'}
					]
				}
			]
		},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; right: 0px; top: 0px;', components: [
			{content:'Small', small:true},
			{kind: 'moon.ContextualPopup'}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; right: 0px; top: 13%;', components: [
			{content:'Nested Radio', small:true},
				{name:'nestedRadioPopup', kind: 'moon.ContextualPopup', components:[
					{name:'nestedRadioGroup', kind: 'moon.RadioItemGroup', components: [
						{content: 'Creek', selected: true},
						{content: 'River'},
						{content: 'Ocean'}
					]},
					{components:[
						{content: 'Radio Group Value'},
						{name: 'nestedRadioValue'}
					]},
					{name: 'nestedRadioDismissButton',
					 kind: 'moon.ToggleButton',
					 style:'margin-top:5px',
					 small: true,
					 toggleOnLabel: 'select dismiss on',
					 toggleOffLabel: 'select dismiss off'
					},
				]}
			]
		},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; left: 0px; top: 25%;', components: [
			{content: 'Left'},
			{
				kind: 'moon.ContextualPopup',
				classes: 'moon-3h moon-4v',
				components: [
					{content:'Item 1'},
					{content:'Item 2'},
					{content:'Item 3'}
				]
			}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; right: 0px; top: 25%;', components: [
			{content:'Right', small:true},
			{kind: 'moon.ContextualPopup', components: [
				{content:'Outside scroller', kind: 'moon.Item'},
				{classes: 'moon-8h moon-6v', components: [
					{kind: 'moon.Scroller', classes: 'enyo-fill', components: [
						{content:'testing 1', kind: 'moon.Item'},
						{content:'testing 2', kind: 'moon.Item'},
						{content:'testing 3', kind: 'moon.Item'},
						{content:'testing 4', kind: 'moon.Item'},
						{content:'testing 5', kind: 'moon.Item'},
						{content:'testing 6', kind: 'moon.Item'},
						{content:'testing 7', kind: 'moon.Item'},
						{content:'testing 8', kind: 'moon.Item'},
						{content:'testing 9', kind: 'moon.Item'},
						{content:'testing 10', kind: 'moon.Item'},
						{content:'testing 12', kind: 'moon.Item'},
						{content:'testing 13', kind: 'moon.Item'},
						{content:'testing 14', kind: 'moon.Item'},
						{content:'testing 15', kind: 'moon.Item'},
						{content:'testing 16', kind: 'moon.Item'},
						{content:'testing 17', kind: 'moon.Item'},
						{content:'testing 18', kind: 'moon.Item'},
						{content:'testing 19', kind: 'moon.Item'}
					]}

				]}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; left: 0px; top: 45%;', components: [
			{content: 'Wide'},
			{kind: 'moon.ContextualPopup', classes: 'moon-6h moon-4v', components: [
				{kind: 'moon.Scroller', classes: 'enyo-fill', components:[
					{content:'testing 1'},
					{content:'testing 2'}
				]}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; right: 0px; top: 45%;', components: [
			{content:'Long but Small Button with truncation', small:true},
			{kind: 'moon.ContextualPopup', components: [
				{content:'Outside scroller', kind: 'moon.Item'},
				{classes: 'moon-16v', components: [
					{kind: 'moon.Scroller', classes: 'enyo-fill', components: [
						{content:'testing 1', kind: 'moon.Item'},
						{content:'testing 2', kind: 'moon.Item'},
						{content:'testing 3', kind: 'moon.Item'},
						{content:'testing 4', kind: 'moon.Item'},
						{content:'testing 5', kind: 'moon.Item'},
						{content:'testing 6', kind: 'moon.Item'},
						{content:'testing 7', kind: 'moon.Item'},
						{content:'testing 8', kind: 'moon.Item'},
						{content:'testing 9', kind: 'moon.Item'},
						{content:'testing 10', kind: 'moon.Item'},
						{content:'testing 12', kind: 'moon.Item'},
						{content:'testing 13', kind: 'moon.Item'},
						{content:'testing 14', kind: 'moon.Item'},
						{content:'testing 15', kind: 'moon.Item'},
						{content:'testing 16', kind: 'moon.Item'},
						{content:'testing 17', kind: 'moon.Item'},
						{content:'testing 18', kind: 'moon.Item'},
						{content:'testing 19', kind: 'moon.Item'}
					]}

				]}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; left: 0px; top: 65%;', components: [
			{content: 'Deactivated', disabled:true},
			{kind: 'moon.ContextualPopup', classes: 'moon-6h moon-4v', components: [
				{kind: 'moon.Scroller', classes: 'enyo-fill', components:[
					{content:'testing 1'},
					{content:'testing 2'}
				]}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', style:'position: absolute; right: 0px; top: 65%;', components: [
			{content: 'Small Deactivated', small:true, disabled:true},
			{kind: 'moon.ContextualPopup', classes: 'moon-6h moon-4v', components: [
				{kind: 'moon.Scroller', classes: 'enyo-fill', components:[
					{content:'testing 1'},
					{content:'testing 2'}
				]}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', style: 'position: absolute; left: 0px; bottom: 0px;', components: [
			{content: 'Spotlight Modal'},
			{
				kind: 'moon.ContextualPopup',
				name: 'buttonPopup',
				classes: 'moon-8h moon-8v',
				modal: true,
				autoDismiss: false,
				spotlightModal: true,
				components: [
					{kind: 'moon.Scroller', horizontal: 'auto', classes: 'enyo-fill', components: [
						{kind: 'moon.Button', content: 'Button'},
						{kind: 'moon.ToggleButton', content: 'SpotlightModal', value: true, ontap: 'buttonToggled'},
						{tag: 'br'},
						{tag: 'br'},
						{kind: 'moon.InputDecorator', spotlight: true, components: [
							{kind: 'moon.Input', placeholder: 'USERNAME'}
						]}
					]}
				]
			}
		]},
		{kind: 'moon.ContextualPopupDecorator', style: 'position: absolute; right: 0px; bottom: 0px;', components: [
			{content: 'Spottable', small:true},
			{
				kind: 'moon.ContextualPopup',
				classes: 'moon-9h moon-4v',
				components: [
					{kind: 'moon.Scroller', horizontal: 'auto', classes: 'enyo-fill', components: [
						{kind: 'moon.Button', content: 'Button 1'},
						{kind: 'moon.Button', content: 'Button 2'},
						{kind: 'moon.Button', content: 'Button 3'}
					]}
				]
			}
		]},
		{style: 'position: absolute; left: 30%; top: 25%;', components: [
			{kind: 'moon.Divider', content: 'Button Position', classes: 'radioItemFont'},
			{kind: moon.InputDecorator, components: [
				{kind: 'moon.Input', style: 'width: 120px', name:'leftInput', placeholder: 'left (px or %)', classes: 'radioItemFont'}
			]},
			{kind: 'moon.InputDecorator', components: [
				{kind: 'moon.Input', style: 'width: 120px', name:'topInput', placeholder: 'top (px or %)', classes: 'radioItemFont'}
			]},
			{kind: 'moon.Button', small: true, content: 'Set Position', ontap: 'setPosition'}
		]},
		{style: 'position: absolute; left: 30%; top: 50%; ', components: [
			{kind: 'moon.Divider', content: 'Popup Direction', classes: 'radioItemFont'},
			{kind: 'moon.RadioItemGroup', onActivate: 'groupChanged', components: [
				{content: 'none', classes: 'radioItemFont'},
				{content: 'left', classes: 'radioItemFont'},
				{content: 'right', classes: 'radioItemFont'},
				{content: 'top', classes: 'radioItemFont'},
				{content: 'bottom', classes: 'radioItemFont'}
			]}
		]},
		{kind: 'moon.ContextualPopupDecorator', name: 'directionButton', style: 'position: absolute; left: 40%; top: 70%;', components: [
			{content: 'Direction'},
			{
				kind: 'moon.ContextualPopup',
				name: 'directionContext',
				classes: 'moon-4v',
				components: [
					{kind: moon.Scroller, horizontal: 'auto', classes: 'enyo-fill', components: [
						{kind: moon.Button, content: 'Button 1'},
						{kind: moon.Button, content: 'Button 2'}
					]}
				]
			}
		]}
	],
	bindings: [
		{from: '.$.nestedRadioGroup.active.content', to: '.$.nestedRadioValue.content', transform: function(val){
			this.dismissRadioSelection();
			return val;
		}}
	],
	buttonToggled: function(inSender, inEvent) {
		this.$.buttonPopup.setSpotlightModal(inSender.getActive());
		this.$.buttonPopup.setAutoDismiss(!inSender.getActive());
	},
	dismissRadioSelection: function(){
		if(this.$.nestedRadioDismissButton.value) this.$.nestedRadioPopup.hide();
	},
	setPosition: function(){
		this.$.directionButton.applyStyle('left', this.$.leftInput.getValue() === '' ? '40%' : this.$.leftInput.getValue());
		this.$.directionButton.applyStyle('top', this.$.topInput.getValue() === '' ? '70%' : this.$.topInput.getValue());
	},
	groupChanged: function(inSender, inEvent) {
		if(inEvent.originator.getActive() && inEvent.originator.kind === 'moon.RadioItem') {
			var selected = inEvent.originator.getContent();
			this.$.directionContext.set('direction', selected);
		}
	}
});<|endoftext|>


<commit_before>/* global sauce */

// NOTE: Must be assigned to self and have matching name for FF
self.sauceBaseInit = function sauceBaseInit() {
    'use strict';

    self.sauce = self.sauce || {};

    function buildPath(path) {
        let offt = self;
        for (const x of path) {
            if (!offt[x]) {
                offt[x] = {};
            }
            offt = offt[x];
        }
        return offt;
    }

    sauce.ns = function(ns, callback) {
        const offt = buildPath(`sauce.${ns}`.split('.'));
        const assignments = callback && callback(offt);
        if (assignments) {
            Object.assign(offt, assignments);
        }
        return offt;
    };

    sauce.stringDigest = function(algo, input) {
        if (typeof input !== 'string') {
            throw new TypeError('Input should string');
        }
        const encoder = new TextEncoder();
        const data = encoder.encode(input);
        return sauce.digest(algo, data);
    };

    sauce.digest = async function(algo, data) {
        const hash = await crypto.subtle.digest(algo, data);
        const hashArray = Array.from(new Uint8Array(hash));
        return hashArray.map(x => x.toString(16).padStart(2, '0')).join('');
    };

    sauce.sha1 = function(input) {
        return sauce.stringDigest('SHA-1', input);
    };

    sauce.sha256 = function(input) {
        return sauce.stringDigest('SHA-256', input);
    };

    sauce.theme = function(name) {
        const doc = document.documentElement;
        const classes = Array.from(doc.classList);
        for (const x of classes) {
            if (x.startsWith('sauce-theme-')) {
                doc.classList.remove(x);
            }
        }
        if (name) {
            doc.classList.add('sauce-theme-enabled');
            doc.classList.add(`sauce-theme-${name}`);
        }
    };

    const _modules = {};
    sauce.getModule = async function(url) {
        // Note: modules are only supported in the site context and the background page.
        // Content scripts of the extention itself do NOT work and cannot work without
        // browser support due to isolation issues.
        if (!_modules[url]) {
            const script = document.createElement('script');
            const doneEvent = 'sauceModuleImportDone-' + (Date.now() + Math.random());
            const extUrl = self.browser ? self.browser.runtime.getURL('') : sauce.extUrl;
            _modules[url] = await new Promise((resolve, reject) => {
                script.addEventListener('error', ev => reject(new Error(`Module load error: ${url}`)));
                script.type = 'module';
                script.src = extUrl + `src/common/module-loader.mjs?doneEvent=${doneEvent}&script=${url}`;
                function onDone(ev) {
                    document.removeEventListener(doneEvent, onDone);
                    script.remove();
                    if (ev.error) {
                        reject(ev.error);
                    } else {
                        resolve(ev.module);
                    }
                }
                document.addEventListener(doneEvent, onDone);
                document.documentElement.appendChild(script);
            });
        }
        return _modules[url];
    };

    sauce.loadModule = async function(namespace, url) {
        const path = namespace.split('.');
        const root = buildPath(path.slice(0, -1));
        const name = path.pop();
        if (!root[name]) {
            root[name] = await sauce.getModule(url);
        }
        return root[name];
    };

    sauce.propDefined = function(propertyAccessor, callback, options) {
        if (typeof callback === 'object' && options === undefined) {
            options = callback;
            callback = null;
        } else {
            options = options || {};
        }
        const ourListeners = [];
        function addListener(propDesc, fn) {
            propDesc.set.listeners.push(fn);
            ourListeners.push({fn, propDesc});
        }
        return new Promise(resolve => {
            function catchDefine(obj, props) {
                const prop = props[0];
                function onSet(value) {
                    if (props.length > 1) {
                        if (Object.isExtensible(value)) {
                            catchDefine(value, props.slice(1));
                        }
                    } else {
                        if (options.once) {
                            for (const {fn, propDesc} of ourListeners) {
                                const idx = propDesc.set.listeners.indexOf(fn);
                                propDesc.set.listeners.splice(idx, 1);
                                if (!propDesc.set.listeners.length) {
                                    Object.defineProperty(obj, prop, {
                                        value: obj[prop],
                                        configurable: true,
                                        writable: true,
                                        enumerable: propDesc.enumerable
                                    });
                                }
                            }
                        }
                        if (callback) {
                            callback(value);
                        }
                        resolve(value);
                    }
                }
                const curValue = obj[prop];
                if (curValue !== undefined) {  // Not stoked on this test.
                    onSet(curValue);
                    if (options.once) {
                        return;  // Just walk the props.
                    }
                }
                const propDesc = Object.getOwnPropertyDescriptor(obj, prop);
                if (propDesc && propDesc.set && propDesc.set.listeners) {
                    addListener(propDesc, onSet);
                } else if (!propDesc || (propDesc.configurable && !propDesc.set)) {
                    let internalValue = propDesc ? propDesc.value : undefined;
                    const set = function(value) {
                        if (Object.is(internalValue, value)) {
                            return;
                        }
                        internalValue = value;
                        for (const fn of Array.from(set.listeners)) {
                            fn(value);
                        }
                    };
                    set.listeners = [];
                    Object.defineProperty(obj, prop, {
                        enumerable: true,
                        configurable: true,
                        get: () => internalValue,
                        set
                    });
                    addListener(Object.getOwnPropertyDescriptor(obj, prop), onSet);
                } else if (!options.once) {
                    console.error("Value already exists, consider using `once` option");
                    throw new TypeError("Unconfigurable");
                }
            }
            catchDefine(options.root || self, propertyAccessor.split('.'));
        });
    };

    class Benchmark {
        constructor(label) {
            this.label = label;
            this.steps = [];
            this.count = 0;
            this.times = {};
        }

        _step(key) {
            if (this.steps.length && !this.times[key]) {
                this.times[key] = 0;
            }
            this.steps.push({
                key,
                ts: Date.now()
            });
        }

        _makeTimingLog(startIndex, endIndex) {
            const step = this.steps[startIndex];
            const nextStep = this.steps[endIndex];
            let elapsed = 0;
            for (let i = startIndex + 1; i <= endIndex; i++) {
                const step = this.steps[i];
                elapsed += this.times[step.key];
            }
            const avg = Number((elapsed / this.count).toFixed(2)).toLocaleString();
            return `${step.key}-${nextStep.key}: sum=${elapsed.toLocaleString()}ms, avg=${avg}ms`;
        }

        displayStats() {
            const logs = [];
            if (this.steps.length > 2) {
                for (let i = 0; i < this.steps.length - 1; i++) {
                    logs.push(this._makeTimingLog(i, i + 1));
                }
            }
            logs.push(this._makeTimingLog(0, this.steps.length - 1));
            this._lastDisplay = Date.now();
            console.group('Benchmark', this.label, 'count', this.count);
            for (const x of logs) {
                console.info(x);
            }
            console.groupEnd();
        }

        enter() {
            this.steps.length = 0;
            this._step('start');
        }

        step(label) {
            this._step(label || String.fromCharCode(65 + this.steps.length));
        }

        leave() {
            this._step('finish');
            this.count++;
            for (let i = this.steps.length - 1; i > 0; i--) {
                const step = this.steps[i];
                const prevStep = this.steps[i - 1];
                this.times[step.key] += step.ts - prevStep.ts;
            }
            if (!this._schedDisplay) {
                this._schedDisplay = setTimeout(() => {
                    this._schedDisplay = null;
                    this.displayStats();
                }, 1000);
            }
        }
    }
    sauce.Benchmark = Benchmark;

    class IDBExpirationBucket {
        static async factory() {
            const instance = new this();
            instance.db = await new Promise((resolve, reject) => {
                const request = indexedDB.open('SauceCache', 1);
                request.addEventListener('error', ev => reject(request.error));
                request.addEventListener('success', ev => resolve(ev.target.result));
                request.addEventListener('blocked', ev => reject(new Error('Blocked by existing DB connection')));
                request.addEventListener('upgradeneeded', ev => {
                    const db = ev.target.result;
                    if (ev.oldVersion < 1) {
                        const store = db.createObjectStore("entries", {autoIncrement: true});
                        store.createIndex('bucket-expiration', ['bucket', 'expiration']);
                        store.createIndex('bucket-key', ['bucket', 'key'], {unique: true});
                    }
                });
            });
            return instance;
        }

        requestPromise(req) {
            return new Promise((resolve, reject) => {
                req.addEventListener('error', ev => reject(req.error));
                req.addEventListener('success', ev => resolve(req.result));
            });
        }

        async _storeCall(transactionMode, onStore) {
            const t = this.db.transaction('entries', transactionMode);
            const store = t.objectStore('entries');
            const result = await onStore(store);
            return await new Promise((resolve, reject) => {
                t.addEventListener('abort', ev => reject('Transaction Abort'));
                t.addEventListener('error', ev => reject(t.error));
                t.addEventListener('complete', ev => resolve(result));
            });
        }

        async get(keyOrRange) {
            return await this._storeCall('readonly', async store => {
                const index = store.index('bucket-key');
                return await this.requestPromise(index.get(keyOrRange));
            });
        }

        async getMany(keyOrRangeArray) {
            return await this._storeCall('readonly', async store => {
                const index = store.index('bucket-key');
                return await Promise.all(keyOrRangeArray.map(x =>
                    this.requestPromise(index.get(x))));
            });
        }

        async put(data) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                const key = await this.requestPromise(index.getKey([data.bucket, data.key]));
                return await this.requestPromise(store.put(data, key));
            });
        }

        async putMany(dataArray) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                await Promise.all(dataArray.map(async data => {
                    const key = await this.requestPromise(index.getKey([data.bucket, data.key]));
                    return await this.requestPromise(store.put(data, key));
                }));
            });
        }

        async delete(keyOrRange) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                const key = await this.requestPromise(index.getKey(keyOrRange));
                return await this.requestPromise(store.delete(key));
            });
        }

        async purgeExpired(bucket) {
            return await this._storeCall('readwrite', async store => {
                const now = Date.now();
                const index = store.index('bucket-expiration');
                const cursorReq = index.openCursor(IDBKeyRange.bound([bucket, -Infinity], [bucket, now]));
                return await new Promise((resolve, reject) => {
                    let count = 0;
                    cursorReq.addEventListener('error', ev => reject(cursorReq.error));
                    cursorReq.addEventListener('success', async ev => {
                        try {
                            const cursor = ev.target.result;
                            if (!cursor) {
                                resolve(count);
                                return;
                            }
                            await this.requestPromise(cursor.delete());
                            count++;
                            cursor.continue();
                        } catch(e) {
                            reject(e);
                        }
                    });
                });
            });
        }
    }

    class TTLCache {
        constructor(bucket, ttl) {
            this.bucket = bucket;
            this.ttl = ttl;
            this._initing = this.init();
        }

        async init() {
            this.idb = await IDBExpirationBucket.factory();
            this.gc();  // bg okay
        }

        async gc() {
            for (let sleep = 10000;; sleep = Math.min(sleep + 1000, 300 * 1000)) {
                await new Promise(resolve => setTimeout(resolve, sleep));
                const count = await this.idb.purgeExpired(this.bucket);
                if (count) {
                    console.info(`Flushed ${count} objects from TTL cache`);
                }
            }
        }

        async getEntry(key) {
            await this._initing;
            const entry = await this.idb.get([this.bucket, key]);
            if (entry && entry.expiration > Date.now()) {
                return entry;
            }
        }

        async get(key) {
            const entry = await this.getEntry(key);
            return entry && entry.value;
        }

        async getEntries(keys) {
            await this._initing;
            const entries = await this.idb.getMany(keys.map(k => [this.bucket, k]));
            const now = Date.now();
            return entries.filter(x => x && x.expiration > now);
        }

        async getObject(keys) {
            const entries = await this.getEntries(keys);
            const obj = {};
            for (const x of entries) {
                obj[x.key] = x.value;
            }
            return obj;
        }

        async set(key, value, options={}) {
            await this._initing;
            const ttl = options.ttl || this.ttl;
            const created = Date.now();
            const expiration = created + ttl;
            await this.idb.put({
                bucket: this.bucket,
                key,
                created,
                expiration,
                value
            });
        }

        async setObject(obj, options={}) {
            await this._initing;
            const ttl = options.ttl || this.ttl;
            const created = Date.now();
            const expiration = created + ttl;
            await this.idb.putMany(Object.entries(obj).map(([key, value]) => ({
                bucket: this.bucket,
                key,
                created,
                expiration,
                value,
            })));
        }

        async delete(key) {
            await this._initing;
            await this.idb.delete([this.bucket, key]);
        }
    }
    sauce.cache = {
        TTLCache
    };
};
<commit_msg>Add Cache and IndexedDB async iteration of buckets.<commit_after>/* global sauce */

// NOTE: Must be assigned to self and have matching name for FF
self.sauceBaseInit = function sauceBaseInit() {
    'use strict';

    self.sauce = self.sauce || {};

    function buildPath(path) {
        let offt = self;
        for (const x of path) {
            if (!offt[x]) {
                offt[x] = {};
            }
            offt = offt[x];
        }
        return offt;
    }

    sauce.ns = function(ns, callback) {
        const offt = buildPath(`sauce.${ns}`.split('.'));
        const assignments = callback && callback(offt);
        if (assignments) {
            Object.assign(offt, assignments);
        }
        return offt;
    };

    sauce.stringDigest = function(algo, input) {
        if (typeof input !== 'string') {
            throw new TypeError('Input should string');
        }
        const encoder = new TextEncoder();
        const data = encoder.encode(input);
        return sauce.digest(algo, data);
    };

    sauce.digest = async function(algo, data) {
        const hash = await crypto.subtle.digest(algo, data);
        const hashArray = Array.from(new Uint8Array(hash));
        return hashArray.map(x => x.toString(16).padStart(2, '0')).join('');
    };

    sauce.sha1 = function(input) {
        return sauce.stringDigest('SHA-1', input);
    };

    sauce.sha256 = function(input) {
        return sauce.stringDigest('SHA-256', input);
    };

    sauce.theme = function(name) {
        const doc = document.documentElement;
        const classes = Array.from(doc.classList);
        for (const x of classes) {
            if (x.startsWith('sauce-theme-')) {
                doc.classList.remove(x);
            }
        }
        if (name) {
            doc.classList.add('sauce-theme-enabled');
            doc.classList.add(`sauce-theme-${name}`);
        }
    };

    const _modules = {};
    sauce.getModule = async function(url) {
        // Note: modules are only supported in the site context and the background page.
        // Content scripts of the extention itself do NOT work and cannot work without
        // browser support due to isolation issues.
        if (!_modules[url]) {
            const script = document.createElement('script');
            const doneEvent = 'sauceModuleImportDone-' + (Date.now() + Math.random());
            const extUrl = self.browser ? self.browser.runtime.getURL('') : sauce.extUrl;
            _modules[url] = await new Promise((resolve, reject) => {
                script.addEventListener('error', ev => reject(new Error(`Module load error: ${url}`)));
                script.type = 'module';
                script.src = extUrl + `src/common/module-loader.mjs?doneEvent=${doneEvent}&script=${url}`;
                function onDone(ev) {
                    document.removeEventListener(doneEvent, onDone);
                    script.remove();
                    if (ev.error) {
                        reject(ev.error);
                    } else {
                        resolve(ev.module);
                    }
                }
                document.addEventListener(doneEvent, onDone);
                document.documentElement.appendChild(script);
            });
        }
        return _modules[url];
    };

    sauce.loadModule = async function(namespace, url) {
        const path = namespace.split('.');
        const root = buildPath(path.slice(0, -1));
        const name = path.pop();
        if (!root[name]) {
            root[name] = await sauce.getModule(url);
        }
        return root[name];
    };

    sauce.propDefined = function(propertyAccessor, callback, options) {
        if (typeof callback === 'object' && options === undefined) {
            options = callback;
            callback = null;
        } else {
            options = options || {};
        }
        const ourListeners = [];
        function addListener(propDesc, fn) {
            propDesc.set.listeners.push(fn);
            ourListeners.push({fn, propDesc});
        }
        return new Promise(resolve => {
            function catchDefine(obj, props) {
                const prop = props[0];
                function onSet(value) {
                    if (props.length > 1) {
                        if (Object.isExtensible(value)) {
                            catchDefine(value, props.slice(1));
                        }
                    } else {
                        if (options.once) {
                            for (const {fn, propDesc} of ourListeners) {
                                const idx = propDesc.set.listeners.indexOf(fn);
                                propDesc.set.listeners.splice(idx, 1);
                                if (!propDesc.set.listeners.length) {
                                    Object.defineProperty(obj, prop, {
                                        value: obj[prop],
                                        configurable: true,
                                        writable: true,
                                        enumerable: propDesc.enumerable
                                    });
                                }
                            }
                        }
                        if (callback) {
                            callback(value);
                        }
                        resolve(value);
                    }
                }
                const curValue = obj[prop];
                if (curValue !== undefined) {  // Not stoked on this test.
                    onSet(curValue);
                    if (options.once) {
                        return;  // Just walk the props.
                    }
                }
                const propDesc = Object.getOwnPropertyDescriptor(obj, prop);
                if (propDesc && propDesc.set && propDesc.set.listeners) {
                    addListener(propDesc, onSet);
                } else if (!propDesc || (propDesc.configurable && !propDesc.set)) {
                    let internalValue = propDesc ? propDesc.value : undefined;
                    const set = function(value) {
                        if (Object.is(internalValue, value)) {
                            return;
                        }
                        internalValue = value;
                        for (const fn of Array.from(set.listeners)) {
                            fn(value);
                        }
                    };
                    set.listeners = [];
                    Object.defineProperty(obj, prop, {
                        enumerable: true,
                        configurable: true,
                        get: () => internalValue,
                        set
                    });
                    addListener(Object.getOwnPropertyDescriptor(obj, prop), onSet);
                } else if (!options.once) {
                    console.error("Value already exists, consider using `once` option");
                    throw new TypeError("Unconfigurable");
                }
            }
            catchDefine(options.root || self, propertyAccessor.split('.'));
        });
    };

    class Benchmark {
        constructor(label) {
            this.label = label;
            this.steps = [];
            this.count = 0;
            this.times = {};
        }

        _step(key) {
            if (this.steps.length && !this.times[key]) {
                this.times[key] = 0;
            }
            this.steps.push({
                key,
                ts: Date.now()
            });
        }

        _makeTimingLog(startIndex, endIndex) {
            const step = this.steps[startIndex];
            const nextStep = this.steps[endIndex];
            let elapsed = 0;
            for (let i = startIndex + 1; i <= endIndex; i++) {
                const step = this.steps[i];
                elapsed += this.times[step.key];
            }
            const avg = Number((elapsed / this.count).toFixed(2)).toLocaleString();
            return `${step.key}-${nextStep.key}: sum=${elapsed.toLocaleString()}ms, avg=${avg}ms`;
        }

        displayStats() {
            const logs = [];
            if (this.steps.length > 2) {
                for (let i = 0; i < this.steps.length - 1; i++) {
                    logs.push(this._makeTimingLog(i, i + 1));
                }
            }
            logs.push(this._makeTimingLog(0, this.steps.length - 1));
            this._lastDisplay = Date.now();
            console.group('Benchmark', this.label, 'count', this.count);
            for (const x of logs) {
                console.info(x);
            }
            console.groupEnd();
        }

        enter() {
            this.steps.length = 0;
            this._step('start');
        }

        step(label) {
            this._step(label || String.fromCharCode(65 + this.steps.length));
        }

        leave() {
            this._step('finish');
            this.count++;
            for (let i = this.steps.length - 1; i > 0; i--) {
                const step = this.steps[i];
                const prevStep = this.steps[i - 1];
                this.times[step.key] += step.ts - prevStep.ts;
            }
            if (!this._schedDisplay) {
                this._schedDisplay = setTimeout(() => {
                    this._schedDisplay = null;
                    this.displayStats();
                }, 1000);
            }
        }
    }
    sauce.Benchmark = Benchmark;

    class IDBExpirationBucket {
        static async factory() {
            const instance = new this();
            instance.db = await new Promise((resolve, reject) => {
                const request = indexedDB.open('SauceCache', 1);
                request.addEventListener('error', ev => reject(request.error));
                request.addEventListener('success', ev => resolve(ev.target.result));
                request.addEventListener('blocked', ev => reject(new Error('Blocked by existing DB connection')));
                request.addEventListener('upgradeneeded', ev => {
                    const db = ev.target.result;
                    if (ev.oldVersion < 1) {
                        const store = db.createObjectStore("entries", {autoIncrement: true});
                        store.createIndex('bucket-expiration', ['bucket', 'expiration']);
                        store.createIndex('bucket-key', ['bucket', 'key'], {unique: true});
                    }
                });
            });
            return instance;
        }

        requestPromise(req) {
            return new Promise((resolve, reject) => {
                req.addEventListener('error', ev => reject(req.error));
                req.addEventListener('success', ev => resolve(req.result));
            });
        }

        async _storeCall(transactionMode, onStore) {
            const t = this.db.transaction('entries', transactionMode);
            const store = t.objectStore('entries');
            const result = await onStore(store);
            return await new Promise((resolve, reject) => {
                t.addEventListener('abort', ev => reject('Transaction Abort'));
                t.addEventListener('error', ev => reject(t.error));
                t.addEventListener('complete', ev => resolve(result));
            });
        }

        async get(keyOrRange) {
            return await this._storeCall('readonly', async store => {
                const index = store.index('bucket-key');
                return await this.requestPromise(index.get(keyOrRange));
            });
        }

        async getMany(keyOrRangeArray) {
            return await this._storeCall('readonly', async store => {
                const index = store.index('bucket-key');
                return await Promise.all(keyOrRangeArray.map(x =>
                    this.requestPromise(index.get(x))));
            });
        }

        async put(data) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                const key = await this.requestPromise(index.getKey([data.bucket, data.key]));
                return await this.requestPromise(store.put(data, key));
            });
        }

        async putMany(dataArray) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                await Promise.all(dataArray.map(async data => {
                    const key = await this.requestPromise(index.getKey([data.bucket, data.key]));
                    return await this.requestPromise(store.put(data, key));
                }));
            });
        }

        async delete(keyOrRange) {
            await this._storeCall('readwrite', async store => {
                const index = store.index('bucket-key');
                const key = await this.requestPromise(index.getKey(keyOrRange));
                return await this.requestPromise(store.delete(key));
            });
        }

        async purgeExpired(bucket) {
            return await this._storeCall('readwrite', async store => {
                const now = Date.now();
                const index = store.index('bucket-expiration');
                const cursorReq = index.openCursor(IDBKeyRange.bound([bucket, -Infinity], [bucket, now]));
                return await new Promise((resolve, reject) => {
                    let count = 0;
                    cursorReq.addEventListener('error', ev => reject(cursorReq.error));
                    cursorReq.addEventListener('success', async ev => {
                        try {
                            const cursor = ev.target.result;
                            if (!cursor) {
                                resolve(count);
                                return;
                            }
                            await this.requestPromise(cursor.delete());
                            count++;
                            cursor.continue();
                        } catch(e) {
                            reject(e);
                        }
                    });
                });
            });
        }

        async *iter(bucket, options={}) {
            const filter = options.filter;
            const t = this.db.transaction('entries', 'readonly');
            const store = t.objectStore('entries');
            const index = store.index('bucket-expiration');
            const cursorReq = index.openCursor(IDBKeyRange.bound([bucket, Date.now()], [bucket, Infinity]));
            let resolve;
            let reject;
            // Callbacks won't invoke until we release control of the event loop, so this is safe..
            cursorReq.addEventListener('error', ev => reject(cursorReq.error));
            cursorReq.addEventListener('success', ev => resolve(ev.target.result));
            for (;;) {
                const cursor = await new Promise((_resolve, _reject) => {
                    resolve = _resolve;
                    reject = _reject;
                });
                if (!cursor) {
                    return;
                }
                if (!filter || filter(cursor.key, cursor.value)) {
                    yield cursor.value;
                }
                cursor.continue();
            }
        }
    }

    class TTLCache {
        constructor(bucket, ttl) {
            this.bucket = bucket;
            this.ttl = ttl;
            this._initing = this.init();
        }

        async init() {
            this.idb = await IDBExpirationBucket.factory();
            this.gc();  // bg okay
        }

        async gc() {
            for (let sleep = 10000;; sleep = Math.min(sleep + 1000, 300 * 1000)) {
                await new Promise(resolve => setTimeout(resolve, sleep));
                const count = await this.idb.purgeExpired(this.bucket);
                if (count) {
                    console.info(`Flushed ${count} objects from TTL cache`);
                }
            }
        }

        async getEntry(key) {
            await this._initing;
            const entry = await this.idb.get([this.bucket, key]);
            if (entry && entry.expiration > Date.now()) {
                return entry;
            }
        }

        async get(key) {
            const entry = await this.getEntry(key);
            return entry && entry.value;
        }

        async getEntries(keys) {
            await this._initing;
            const entries = await this.idb.getMany(keys.map(k => [this.bucket, k]));
            const now = Date.now();
            return entries.filter(x => x && x.expiration > now);
        }

        async getObject(keys) {
            const entries = await this.getEntries(keys);
            const obj = {};
            for (const x of entries) {
                obj[x.key] = x.value;
            }
            return obj;
        }

        async set(key, value, options={}) {
            await this._initing;
            const ttl = options.ttl || this.ttl;
            const created = Date.now();
            const expiration = created + ttl;
            await this.idb.put({
                bucket: this.bucket,
                key,
                created,
                expiration,
                value
            });
        }

        async setObject(obj, options={}) {
            await this._initing;
            const ttl = options.ttl || this.ttl;
            const created = Date.now();
            const expiration = created + ttl;
            await this.idb.putMany(Object.entries(obj).map(([key, value]) => ({
                bucket: this.bucket,
                key,
                created,
                expiration,
                value,
            })));
        }

        async delete(key) {
            await this._initing;
            await this.idb.delete([this.bucket, key]);
        }

        iter() {
            return this.idb.iter(this.bucket);
        }
    }
    sauce.cache = {
        TTLCache
    };
};
<|endoftext|>