diff --git a/examples/autosizeissue/index.html b/examples/autosizeissue/index.html new file mode 100755 index 000000000..1c56eeda2 --- /dev/null +++ b/examples/autosizeissue/index.html @@ -0,0 +1,698 @@ + + + + + fin-hypergrid Demo + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + +
+
+
+
+
+
+
+
+
+ blah +
+
+
+ + + + + diff --git a/examples/autosizeissue/index.js b/examples/autosizeissue/index.js new file mode 100644 index 000000000..f2a28d56c --- /dev/null +++ b/examples/autosizeissue/index.js @@ -0,0 +1,21471 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o CAVEAT: Not to be confused with Underscore-style .extend() which is something else entirely. I've used the name "extend" here because other packages (like Backbone.js) use it this way. You are free to call it whatever you want when you "require" it, such as `var inherits = require('extend')`. + * + * Provide a constructor as the context and any prototype additions you require in the first argument. + * + * For example, if you wish to be able to extend `BaseConstructor` to a new constructor with prototype overrides and/or additions, basic usage is: + * + * ```javascript + * var Base = require('extend-me').Base; + * var BaseConstructor = Base.extend(basePrototype); // mixes in .extend + * var ChildConstructor = BaseConstructor.extend(childPrototypeOverridesAndAdditions); + * var GrandchildConstructor = ChildConstructor.extend(grandchildPrototypeOverridesAndAdditions); + * ``` + * + * This function (`extend()`) is added to the new extended object constructor as a property `.extend`, essentially making the object constructor itself easily "extendable." (Note: This is a property of each constructor and not a method of its prototype!) + * + * @param {string} [extendedClassName] - This is simply added to the prototype as $$CLASS_NAME. Useful for debugging because all derived constructors appear to have the same name ("Constructor") in the debugger. This property is ignored unless `extend.debug` is explicitly set to a truthy value. + * + * @param {extendedPrototypeAdditionsObject} [prototypeAdditions] - Object with members to copy to new constructor's prototype. Most members will be copied to the prototype. Some members, however, have special meanings as explained in the {@link extendedPrototypeAdditionsObject|type definition} (and may or may not be copied to the prototype). + * + * @property {boolean} [debug] - See parameter `extendedClassName` _(above)_. + * + * @property {object} Base - A convenient base class from which all other classes can be extended. + * + * @memberOf extend-me + */ +function extend(extendedClassName, prototypeAdditions) { + switch (arguments.length) { + case 0: + prototypeAdditions = {}; + break; + case 1: + prototypeAdditions = extendedClassName; + if (typeof prototypeAdditions !== 'object') { + throw 'Single parameter overload must be object.'; + } + extendedClassName = undefined; + break; + case 2: + if (typeof extendedClassName !== 'string' || typeof prototypeAdditions !== 'object') { + throw 'Two parameter overload must be string, object.'; + } + break; + default: + throw 'Too many parameters'; + } + + function Constructor() { + if (prototypeAdditions.preInitialize) { + prototypeAdditions.preInitialize.apply(this, arguments); + } + + initializePrototypeChain.apply(this, arguments); + + if (prototypeAdditions.postInitialize) { + prototypeAdditions.postInitialize.apply(this, arguments); + } + } + + Constructor.extend = extend; + + var prototype = Constructor.prototype = Object.create(this.prototype); + prototype.constructor = Constructor; + + if (extendedClassName && extend.debug) { + prototype.$$CLASS_NAME = extendedClassName; + } + + for (var key in prototypeAdditions) { + if (prototypeAdditions.hasOwnProperty(key)) { + var value = prototypeAdditions[key]; + switch (key) { + case 'initializeOwn': + // already called above; not needed in prototype + break; + case 'aliases': + for (var alias in value) { + if (value.hasOwnProperty(alias)) { + makeAlias(value[alias], alias); + } + } + break; + default: + if (typeof value === 'string' && value[0] === '#') { + makeAlias(value, key.substr(1)); + } else { + prototype[key] = value; + } + } + } + } + + return Constructor; + + function makeAlias(value, key) { // eslint-disable-line no-shadow + prototype[key] = prototypeAdditions[value]; + } +} + +extend.Base = function () {}; +extend.Base.extend = extend; + +/** @typedef {function} extendedConstructor + * @property prototype.super - A reference to the prototype this constructor was extended from. + * @property [extend] - If `prototypeAdditions.extendable` was truthy, this will be a reference to {@link extend.extend|extend}. + */ + +/** @typedef {object} extendedPrototypeAdditionsObject + * @property {function} [initialize] - Additional constructor code for new object. This method is added to the new constructor's prototype. Gets passed new object as context + same args as constructor itself. Called on instantiation after similar function in all ancestors called with same signature. + * @property {function} [initializeOwn] - Additional constructor code for new object. This method is added to the new constructor's prototype. Gets passed new object as context + same args as constructor itself. Called on instantiation after (all) the `initialize` function(s). + * @property {object} [aliases] - Hash of aliases for prototype members in form `{ key: 'member', ... }` where `key` is the name of an alieas and `'member'` is the name of an existing member in the prototype. Each such key is added to the prototype as a reference to the named member. (The `aliases` object itself is *not* added to prototype.) Alternatively: + * @property {string} [keys] - Arbitrary property names defined here with string values starting with a `#` character will alias the actual properties named in the strings (following the `#`). This is an alternative to providing an `aliases` hash, perhaps simpler (though subtler). (Use arbitrary identifiers here; don't use the name `keys`!) + * @property {*} [arbitraryProperties] - Any additional arbitrary properties defined here will be added to the new constructor's prototype. (Use arbitrary identifiers here; don't use the name `aribitraryProperties`!) + */ + +/** @summary Call all `initialize` methods found in prototype chain. + * @desc This recursive routine is called by the constructor. + * 1. Walks back the prototype chain to `Object`'s prototype + * 2. Walks forward to new object, calling any `initialize` methods it finds along the way with the same context and arguments with which the constructor was called. + * @private + * @memberOf extend-me + */ +function initializePrototypeChain() { + var term = this, + args = arguments; + recur(term); + + function recur(obj) { + var proto = Object.getPrototypeOf(obj); + if (proto.constructor !== Object) { + recur(proto); + if (proto.initialize) { + proto.initialize.apply(term, args); + } + } + } +} + +module.exports = extend; + +},{}],4:[function(require,module,exports){ +'use strict'; + +/* eslint-env node, browser */ + +var cssInjector = require('css-injector'); + +/** + * @constructor FinBar + * @summary Create a scrollbar object. + * @desc Creating a scrollbar is a three-step process: + * + * 1. Instantiate the scrollbar object by calling this constructor function. Upon instantiation, the DOM element for the scrollbar (with a single child element for the scrollbar "thumb") is created but is not insert it into the DOM. + * 2. After instantiation, it is the caller's responsibility to insert the scrollbar, {@link FinBar#bar|this.bar}, into the DOM. + * 3. After insertion, the caller must call {@link FinBar#resize|resize()} at least once to size and position the scrollbar and its thumb. After that, `resize()` should also be called repeatedly on resize events (as the content element is being resized). + * + * Suggested configurations: + * * _**Unbound**_
+ * The scrollbar serves merely as a simple range (slider) control. Omit both `options.onchange` and `options.content`. + * * _**Bound to virtual content element**_
+ * Virtual content is projected into the element using a custom event handler supplied by the programmer in `options.onchange`. A typical use case would be to handle scrolling of the virtual content. Other use cases include data transformations, graphics transformations, _etc._ + * * _**Bound to real content**_
+ * Set `options.content` to the "real" content element but omit `options.onchange`. This will cause the scrollbar to use the built-in event handler (`this.scrollRealContent`) which implements smooth scrolling of the content element within the container. + * + * @param {finbarOptions} [options={}] - Options object. See the type definition for member details. + */ +function FinBar(options) { + + // make bound versions of all the mouse event handler + var bound = this._bound = {}; + for (key in handlersToBeBound) { + bound[key] = handlersToBeBound[key].bind(this); + } + + /** + * @name thumb + * @summary The generated scrollbar thumb element. + * @desc The thumb element's parent element is always the {@link FinBar#bar|bar} element. + * + * This property is typically referenced internally only. The size and position of the thumb element is maintained by `_calcThumb()`. + * @type {Element} + * @memberOf FinBar.prototype + */ + var thumb = document.createElement('div'); + thumb.classList.add('thumb'); + thumb.onclick = bound.shortStop; + thumb.onmouseover = bound.onmouseover; + this.thumb = thumb; + + /** + * @name bar + * @summary The generated scrollbar element. + * @desc The caller inserts this element into the DOM (typically into the content container) and then calls its {@link FinBar#resize|resize()} method. + * + * Thus the node tree is typically: + * * A **content container** element, which contains: + * * The content element(s) + * * This **scrollbar element**, which in turn contains: + * * The **thumb element** + * + * @type {Element} + * @memberOf FinBar.prototype + */ + var bar = document.createElement('div'); + + bar.classList.add('finbar-vertical'); + + bar.appendChild(thumb); + if (this.paging) { + bar.onclick = bound.onclick; + } + this.bar = bar; + + options = options || {}; + + // presets + this.orientation = 'vertical'; + this._min = this._index = 0; + this._max = 100; + + // options + for (var key in options) { + if (options.hasOwnProperty(key)) { + var option = options[key]; + switch (key) { + + case 'index': + this._index = option; + break; + + case 'range': + validRange(option); + this._min = option.min; + this._max = option.max; + this.contentSize = option.max - option.min + 1; + break; + + default: + if ( + key.charAt(0) !== '_' && + typeof FinBar.prototype[key] !== 'function' + ) { + // override prototype defaults for standard ; + // extend with additional properties (for use in onchange event handlers) + this[key] = option; + } + break; + + } + } + } + + cssInjector(cssFinBars, 'finbar-base', options.cssStylesheetReferenceElement); +} + +FinBar.prototype = { + + /** + * @summary The scrollbar orientation. + * @desc Set by the constructor to either `'vertical'` or `'horizontal'`. See the similarly named property in the {@link finbarOptions} object. + * + * Useful values are `'vertical'` (the default) or `'horizontal'`. + * + * Setting this property resets `this.oh` and `this.deltaProp` and changes the class names so as to reposition the scrollbar as per the CSS rules for the new orientation. + * @default 'vertical' + * @type {string} + * @memberOf FinBar.prototype + */ + set orientation(orientation) { + if (orientation === this._orientation) { + return; + } + + this._orientation = orientation; + + /** + * @readonly + * @name oh + * @summary Orientation hash for this scrollbar. + * @desc Set by the `orientation` setter to either the vertical or the horizontal orientation hash. The property should always be synchronized with `orientation`; do not update directly! + * + * This object is used internally to access scrollbars' DOM element properties in a generalized way without needing to constantly query the scrollbar orientation. For example, instead of explicitly coding `this.bar.top` for a vertical scrollbar and `this.bar.left` for a horizontal scrollbar, simply code `this.bar[this.oh.leading]` instead. See the {@link orientationHashType} definition for details. + * + * This object is useful externally for coding generalized {@link finbarOnChange} event handler functions that serve both horizontal and vertical scrollbars. + * @type {orientationHashType} + * @memberOf FinBar.prototype + */ + this.oh = orientationHashes[this._orientation]; + + if (!this.oh) { + error('Invalid value for `options._orientation.'); + } + + /** + * @name deltaProp + * @summary The name of the `WheelEvent` property this scrollbar should listen to. + * @desc Set by the constructor. See the similarly named property in the {@link finbarOptions} object. + * + * Useful values are `'deltaX'`, `'deltaY'`, or `'deltaZ'`. A value of `null` means to ignore mouse wheel events entirely. + * + * The mouse wheel is one-dimensional and only emits events with `deltaY` data. This property is provided so that you can override the default of `'deltaX'` with a value of `'deltaY'` on your horizontal scrollbar primarily to accommodate certain "panoramic" interface designs where the mouse wheel should control horizontal rather than vertical scrolling. Just give `{ deltaProp: 'deltaY' }` in your horizontal scrollbar instantiation. + * + * Caveat: Note that a 2-finger drag on an Apple trackpad emits events with _both_ `deltaX ` and `deltaY` data so you might want to delay making the above adjustment until you can determine that you are getting Y data only with no X data at all (which is a sure bet you on a mouse wheel rather than a trackpad). + + * @type {object|null} + * @memberOf FinBar.prototype + */ + this.deltaProp = this.oh.delta; + + this.bar.className = this.bar.className.replace(/(vertical|horizontal)/g, orientation); + + if (this.bar.style.cssText || this.thumb.style.cssText) { + this.bar.removeAttribute('style'); + this.thumb.removeAttribute('style'); + this.resize(); + } + }, + get orientation() { + return this._orientation; + }, + + /** + * @summary Callback for scroll events. + * @desc Set by the constructor via the similarly named property in the {@link finbarOptions} object. After instantiation, `this.onchange` may be updated directly. + * + * This event handler is called whenever the value of the scrollbar is changed through user interaction. The typical use case is when the content is scrolled. It is called with the `FinBar` object as its context and the current value of the scrollbar (its index, rounded) as the only parameter. + * + * Set this property to `null` to stop emitting such events. + * @type {function(number)|null} + * @memberOf FinBar.prototype + */ + onchange: null, + + /** + * @summary Add a CSS class name to the bar element's class list. + * @desc Set by the constructor. See the similarly named property in the {@link finbarOptions} object. + * + * The bar element's class list will always include `finbar-vertical` (or `finbar-horizontal` based on the current orientation). Whenever this property is set to some value, first the old prefix+orientation is removed from the bar element's class list; then the new prefix+orientation is added to the bar element's class list. This property causes _an additional_ class name to be added to the bar element's class list. Therefore, this property will only add at most one additional class name to the list. + * + * To remove _classname-orientation_ from the bar element's class list, set this property to a falsy value, such as `null`. + * + * > NOTE: You only need to specify an additional class name when you need to have mulltiple different styles of scrollbars on the same page. If this is not a requirement, then you don't need to make a new class; you would just create some additional rules using the same selectors in the built-in stylesheet (../css/finbars.css): + * *`div.finbar-vertical` (or `div.finbar-horizontal`) for the scrollbar + * *`div.finbar-vertical > div` (or `div.finbar-horizontal > div`) for the "thumb." + * + * Of course, your rules should come after the built-ins. + * @type {string} + * @memberOf FinBar.prototype + */ + set classPrefix(prefix) { + if (this._classPrefix) { + this.bar.classList.remove(this._classPrefix + this.orientation); + } + + this._classPrefix = prefix; + + if (prefix) { + this.bar.classList.add(prefix + '-' + this.orientation); + } + }, + get classPrefix() { + return this._classPrefix; + }, + + /** + * @name increment + * @summary Number of scrollbar index units representing a pageful. Used exclusively for paging up and down and for setting thumb size relative to content size. + * @desc Set by the constructor. See the similarly named property in the {@link finbarOptions} object. + * + * Can also be given as a parameter to the {@link FinBar#resize|resize} method, which is pertinent because content area size changes affect the definition of a "pageful." However, you only need to do this if this value is being used. It not used when: + * * you define `paging.up` and `paging.down` + * * your scrollbar is using `scrollRealContent` + * @type {number} + * @memberOf FinBar.prototype + */ + increment: 1, + + /** + * @name barStyles + * @summary Scrollbar styles to be applied by {@link FinBar#resize|resize()}. + * @desc Set by the constructor. See the similarly named property in the {@link finbarOptions} object. + * + * This is a value to be assigned to {@link FinBar#styles|styles} on each call to {@link FinBar#resize|resize()}. That is, a hash of values to be copied to the scrollbar element's style object on resize; or `null` for none. + * + * @see {@link FinBar#style|style} + * @type {finbarStyles|null} + * @memberOf FinBar.prototype + */ + barStyles: null, + + /** + * @name style + * @summary Additional scrollbar styles. + * @desc See type definition for more details. These styles are applied directly to the scrollbar's `bar` element. + * + * Values are adjusted as follows before being applied to the element: + * 1. Included "pseudo-property" names from the scrollbar's orientation hash, {@link FinBar#oh|oh}, are translated to actual property names before being applied. + * 2. When there are margins, percentages are translated to absolute pixel values because CSS ignores margins in its percentage calculations. + * 3. If you give a value without a unit (a raw number), "px" unit is appended. + * + * General notes: + * 1. It is always preferable to specify styles via a stylesheet. Only set this property when you need to specifically override (a) stylesheet value(s). + * 2. Can be set directly or via calls to the {@link FinBar#resize|resize} method. + * 3. Should only be set after the scrollbar has been inserted into the DOM. + * 4. Before applying these new values to the element, _all_ in-line style values are reset (by removing the element's `style` attribute), exposing inherited values (from stylesheets). + * 5. Empty object has no effect. + * 6. Falsey value in place of object has no effect. + * + * > CAVEAT: Do not attempt to treat the object you assign to this property as if it were `this.bar.style`. Specifically, changing this object after assigning it will have no effect on the scrollbar. You must assign it again if you want it to have an effect. + * + * @see {@link FinBar#barStyles|barStyles} + * @type {finbarStyles} + * @memberOf FinBar.prototype + */ + set style(styles) { + var keys = Object.keys(styles = extend({}, styles, this._auxStyles)); + + if (keys.length) { + var bar = this.bar, + barRect = bar.getBoundingClientRect(), + container = this.container || bar.parentElement, + containerRect = container.getBoundingClientRect(), + oh = this.oh; + + // Before applying new styles, revert all styles to values inherited from stylesheets + bar.removeAttribute('style'); + + keys.forEach(function (key) { + var val = styles[key]; + + if (key in oh) { + key = oh[key]; + } + + if (!isNaN(Number(val))) { + val = (val || 0) + 'px'; + } else if (/%$/.test(val)) { + // When bar size given as percentage of container, if bar has margins, restate size in pixels less margins. + // (If left as percentage, CSS's calculation will not exclude margins.) + var oriented = axis[key], + margins = barRect[oriented.marginLeading] + barRect[oriented.marginTrailing]; + if (margins) { + val = parseInt(val, 10) / 100 * containerRect[oriented.size] - margins + 'px'; + } + } + + bar.style[key] = val; + }); + } + }, + + /** + * @readonly + * @name paging + * @summary Enable page up/dn clicks. + * @desc Set by the constructor. See the similarly named property in the {@link finbarOptions} object. + * + * If truthy, listen for clicks in page-up and page-down regions of scrollbar. + * + * If an object, call `.paging.up()` on page-up clicks and `.paging.down()` will be called on page-down clicks. + * + * Changing the truthiness of this value after instantiation currently has no effect. + * @type {boolean|object} + * @memberOf FinBar.prototype + */ + paging: true, + + /** + * @name range + * @summary Setter for the minimum and maximum scroll values. + * @desc Set by the constructor. These values are the limits for {@link FooBar#index|index}. + * + * The setter accepts an object with exactly two numeric properties: `.min` which must be less than `.max`. The values are extracted and the object is discarded. + * + * The getter returns a new object with `.min` and '.max`. + * + * @type {rangeType} + * @memberOf FinBar.prototype + */ + set range(range) { + validRange(range); + this._min = range.min; + this._max = range.max; + this.contentSize = range.max - range.min + 1; + this.index = this.index; // re-clamp + }, + get range() { + return { + min: this._min, + max: this._max + }; + }, + + /** + * @summary Index value of the scrollbar. + * @desc This is the position of the scroll thumb. + * + * Setting this value clamps it to {@link FinBar#min|min}..{@link FinBar#max|max}, scroll the content, and moves thumb. + * + * Getting this value returns the current index. The returned value will be in the range `min`..`max`. It is intentionally not rounded. + * + * Use this value as an alternative to (or in addition to) using the {@link FinBar#onchange|onchange} callback function. + * + * @see {@link FinBar#_setScroll|_setScroll} + * @type {number} + * @memberOf FinBar.prototype + */ + set index(idx) { + idx = Math.min(this._max, Math.max(this._min, idx)); // clamp it + this._setScroll(idx); + // this._setThumbSize(); + }, + get index() { + return this._index; + }, + + /** + * @private + * @summary Move the thumb. + * @desc Also displays the index value in the test panel and invokes the callback. + * @param idx - The new scroll index, a value in the range `min`..`max`. + * @param [scaled=f(idx)] - The new thumb position in pixels and scaled relative to the containing {@link FinBar#bar|bar} element, i.e., a proportional number in the range `0`..`thumbMax`. When omitted, a function of `idx` is used. + * @memberOf FinBar.prototype + */ + _setScroll: function (idx, scaled) { + this._index = idx; + + // Display the index value in the test panel + if (this.testPanelItem && this.testPanelItem.index instanceof Element) { + this.testPanelItem.index.innerHTML = Math.round(idx); + } + + // Call the callback + if (this.onchange) { + this.onchange.call(this, Math.round(idx)); + } + + // Move the thumb + if (scaled === undefined) { + scaled = (idx - this._min) / (this._max - this._min) * this._thumbMax; + } + this.thumb.style[this.oh.leading] = scaled + 'px'; + }, + + scrollRealContent: function (idx) { + var containerRect = this.content.parentElement.getBoundingClientRect(), + sizeProp = this.oh.size, + maxScroll = Math.max(0, this.content[sizeProp] - containerRect[sizeProp]), + //scroll = Math.min(idx, maxScroll); + scroll = (idx - this._min) / (this._max - this._min) * maxScroll; + //console.log('scroll: ' + scroll); + this.content.style[this.oh.leading] = -scroll + 'px'; + }, + + /** + * @summary Recalculate thumb position. + * + * @desc This method recalculates the thumb size and position. Call it once after inserting your scrollbar into the DOM, and repeatedly while resizing the scrollbar (which typically happens when the scrollbar's parent is resized by user. + * + * > This function shifts args if first arg omitted. + * + * @param {number} [increment=this.increment] - Resets {@link FooBar#increment|increment} (see). + * + * @param {finbarStyles} [barStyles=this.barStyles] - (See type definition for details.) Scrollbar styles to be applied to the bar element. + * + * Only specify a `barStyles` object when you need to override stylesheet values. If provided, becomes the new default (`this.barStyles`), for use as a default on subsequent calls. + * + * It is generally the case that the scrollbar's new position is sufficiently described by the current styles. Therefore, it is unusual to need to provide a `barStyles` object on every call to `resize`. + * + * @returns {FinBar} Self for chaining. + * @memberOf FinBar.prototype + */ + resize: function (increment, barStyles) { + var bar = this.bar; + + if (!bar.parentNode) { + return; // not in DOM yet so nothing to do + } + + var container = this.container || bar.parentElement, + containerRect = container.getBoundingClientRect(); + + // shift args if if 1st arg omitted + if (typeof increment === 'object') { + barStyles = increment; + increment = undefined; + } + + this.style = this.barStyles = barStyles || this.barStyles; + + // Bound to real content: Content was given but no onchange handler. + // Set up .onchange, .containerSize, and .increment. + // Note this only makes sense if your index unit is pixels. + if (this.content) { + if (!this.onchange) { + this.onchange = this.scrollRealContent; + this.contentSize = this.content[this.oh.size]; + this._min = 0; + this._max = this.contentSize - 1; + } + } + if (this.onchange === this.scrollRealContent) { + this.containerSize = containerRect[this.oh.size]; + this.increment = this.containerSize / (this.contentSize - this.containerSize) * (this._max - this._min); + } else { + this.containerSize = 1; + this.increment = increment || this.increment; + } + + var index = this.index; + this.testPanelItem = this.testPanelItem || this._addTestPanelItem(); + this._setThumbSize(); + this.index = index; + + if (this.deltaProp !== null) { + container.addEventListener('wheel', this._bound.onwheel); + } + + return this; + }, + + /** + * @summary Shorten trailing end of scrollbar by thickness of some other scrollbar. + * @desc In the "classical" scenario where vertical scroll bar is on the right and horizontal scrollbar is on the bottom, you want to shorten the "trailing end" (bottom and right ends, respectively) of at least one of them so they don't overlay. + * + * This convenience function is an programmatic alternative to hardcoding the correct style with the correct value in your stylesheet; or setting the correct style with the correct value in the {@link FinBar#barStyles|barStyles} object. + * + * @see {@link FinBar#foreshortenBy|foreshortenBy}. + * + * @param {FinBar|null} otherFinBar - Other scrollbar to avoid by shortening this one; `null` removes the trailing space + * @returns {FinBar} For chaining + */ + shortenBy: function (otherFinBar) { return this.shortenEndBy('trailing', otherFinBar); }, + + /** + * @summary Shorten leading end of scrollbar by thickness of some other scrollbar. + * @desc Supports non-classical scrollbar scenarios where vertical scroll bar may be on left and horizontal scrollbar may be on top, in which case you want to shorten the "leading end" rather than the trailing end. + * @see {@link FinBar#shortenBy|shortenBy}. + * @param {FinBar|null} otherFinBar - Other scrollbar to avoid by shortening this one; `null` removes the trailing space + * @returns {FinBar} For chaining + */ + foreshortenBy: function (otherFinBar) { return this.shortenEndBy('leading', otherFinBar); }, + + /** + * @summary Generalized shortening function. + * @see {@link FinBar#shortenBy|shortenBy}. + * @see {@link FinBar#foreshortenBy|foreshortenBy}. + * @param {string} whichEnd - a CSS style property name or an orientation hash name that translates to a CSS style property name. + * @param {FinBar|null} otherFinBar - Other scrollbar to avoid by shortening this one; `null` removes the trailing space + * @returns {FinBar} For chaining + */ + shortenEndBy: function (whichEnd, otherFinBar) { + if (!otherFinBar) { + delete this._auxStyles; + } else if (otherFinBar instanceof FinBar && otherFinBar.orientation !== this.orientation) { + var otherStyle = window.getComputedStyle(otherFinBar.bar), + ooh = orientationHashes[otherFinBar.orientation]; + this._auxStyles = {}; + this._auxStyles[whichEnd] = otherStyle[ooh.thickness]; + } + return this; // for chaining + }, + + /** + * @private + * @summary Sets the proportional thumb size and hides thumb when 100%. + * @desc The thumb size has an absolute minimum of 20 (pixels). + * @memberOf FinBar.prototype + */ + _setThumbSize: function () { + var oh = this.oh, + thumbComp = window.getComputedStyle(this.thumb), + thumbMarginLeading = parseInt(thumbComp[oh.marginLeading]), + thumbMarginTrailing = parseInt(thumbComp[oh.marginTrailing]), + thumbMargins = thumbMarginLeading + thumbMarginTrailing, + barSize = this.bar.getBoundingClientRect()[oh.size], + thumbSize = Math.max(20, barSize * this.containerSize / this.contentSize); + + if (this.containerSize < this.contentSize) { + this.bar.style.visibility = 'visible'; + this.thumb.style[oh.size] = thumbSize + 'px'; + } else { + this.bar.style.visibility = 'hidden'; + } + + /** + * @private + * @name _thumbMax + * @summary Maximum offset of thumb's leading edge. + * @desc This is the pixel offset within the scrollbar of the thumb when it is at its maximum position at the extreme end of its range. + * + * This value takes into account the newly calculated size of the thumb element (including its margins) and the inner size of the scrollbar (the thumb's containing element, including _its_ margins). + * + * NOTE: Scrollbar padding is not taken into account and assumed to be 0 in the current implementation and is assumed to be `0`; use thumb margins in place of scrollbar padding. + * @type {number} + * @memberOf FinBar.prototype + */ + this._thumbMax = barSize - thumbSize - thumbMargins; + + this._thumbMarginLeading = thumbMarginLeading; // used in mousedown + }, + + /** + * @summary Remove the scrollbar. + * @desc Unhooks all the event handlers and then removes the element from the DOM. Always call this method prior to disposing of the scrollbar object. + * @memberOf FinBar.prototype + */ + remove: function () { + this._removeEvt('mousedown'); + this._removeEvt('mousemove'); + this._removeEvt('mouseup'); + + (this.container || this.bar.parentElement)._removeEvt('wheel', this._bound.onwheel); + + this.bar.onclick = + this.thumb.onclick = + this.thumb.onmouseover = + this.thumb.transitionend = + this.thumb.onmouseout = null; + + this.bar.remove(); + }, + + /** + * @private + * @function _addTestPanelItem + * @summary Append a test panel element. + * @desc If there is a test panel in the DOM (typically an `
    ...
` element) with class names of both `this.classPrefix` and `'test-panel'` (or, barring that, any element with class name `'test-panel'`), an `
  • ...
  • ` element will be created and appended to it. This new element will contain a span for each class name given. + * + * You should define a CSS selector `.listening` for these spans. This class will be added to the spans to alter their appearance when a listener is added with that class name (prefixed with 'on'). + * + * (This is an internal function that is called once by the constructor on every instantiation.) + * @returns {Element|undefined} The appended `
  • ...
  • ` element or `undefined` if there is no test panel. + * @memberOf FinBar.prototype + */ + _addTestPanelItem: function () { + var testPanelItem, + testPanelElement = document.querySelector('.' + this._classPrefix + '.test-panel') || document.querySelector('.test-panel'); + + if (testPanelElement) { + var testPanelItemPartNames = [ 'mousedown', 'mousemove', 'mouseup', 'index' ], + item = document.createElement('li'); + + testPanelItemPartNames.forEach(function (partName) { + item.innerHTML += '' + partName.replace('mouse', '') + ''; + }); + + testPanelElement.appendChild(item); + + testPanelItem = {}; + testPanelItemPartNames.forEach(function (partName) { + testPanelItem[partName] = item.getElementsByClassName(partName)[0]; + }); + } + + return testPanelItem; + }, + + _addEvt: function (evtName) { + var spy = this.testPanelItem && this.testPanelItem[evtName]; + if (spy) { spy.classList.add('listening'); } + window.addEventListener(evtName, this._bound['on' + evtName]); + }, + + _removeEvt: function (evtName) { + var spy = this.testPanelItem && this.testPanelItem[evtName]; + if (spy) { spy.classList.remove('listening'); } + window.removeEventListener(evtName, this._bound['on' + evtName]); + } +}; + +function extend(obj) { + for (var i = 1; i < arguments.length; ++i) { + var objn = arguments[i]; + if (objn) { + for (var key in objn) { + obj[key] = objn[key]; + } + } + } + return obj; +} + +function validRange(range) { + var keys = Object.keys(range), + valid = keys.length === 2 && + typeof range.min === 'number' && + typeof range.max === 'number' && + range.min <= range.max; + + if (!valid) { + error('Invalid .range object.'); + } +} + +/** + * @private + * @name handlersToBeBound + * @type {object} + * @desc The functions defined in this object are all DOM event handlers that are bound by the FinBar constructor to each new instance. In other words, the `this` value of these handlers, once bound, refer to the FinBar object and not to the event emitter. "Do not consume raw." + */ +var handlersToBeBound = { + shortStop: function (evt) { + evt.stopPropagation(); + }, + + onwheel: function (evt) { + this.index += evt[this.deltaProp]; + evt.stopPropagation(); + evt.preventDefault(); + }, + + onclick: function (evt) { + var thumbBox = this.thumb.getBoundingClientRect(), + goingUp = evt[this.oh.coordinate] < thumbBox[this.oh.leading]; + + if (typeof this.paging === 'object') { + this.index = this.paging[goingUp ? 'up' : 'down'](Math.round(this.index)); + } else { + this.index += goingUp ? -this.increment : this.increment; + } + + // make the thumb glow momentarily + this.thumb.classList.add('hover'); + var self = this; + this.thumb.addEventListener('transitionend', function waitForIt() { + this.removeEventListener('transitionend', waitForIt); + self._bound.onmouseup(evt); + }); + + evt.stopPropagation(); + }, + + onmouseover: function () { + this.thumb.classList.add('hover'); + this.thumb.onmouseout = this._bound.onmouseout; + this._addEvt('mousedown'); + }, + + onmouseout: function () { + this._removeEvt('mousedown'); + this.thumb.onmouseover = this._bound.onmouseover; + this.thumb.classList.remove('hover'); + }, + + onmousedown: function (evt) { + this._removeEvt('mousedown'); + this.thumb.onmouseover = this.thumb.onmouseout = null; + + var thumbBox = this.thumb.getBoundingClientRect(); + this.pinOffset = evt[this.oh.axis] - thumbBox[this.oh.leading] + this.bar.getBoundingClientRect()[this.oh.leading] + this._thumbMarginLeading; + document.documentElement.style.cursor = 'default'; + + this._addEvt('mousemove'); + this._addEvt('mouseup'); + + evt.stopPropagation(); + evt.preventDefault(); + }, + + onmousemove: function (evt) { + var scaled = Math.min(this._thumbMax, Math.max(0, evt[this.oh.axis] - this.pinOffset)); + var idx = scaled / this._thumbMax * (this._max - this._min) + this._min; + + this._setScroll(idx, scaled); + + evt.stopPropagation(); + evt.preventDefault(); + }, + + onmouseup: function (evt) { + this._removeEvt('mousemove'); + this._removeEvt('mouseup'); + + document.documentElement.style.cursor = 'auto'; + + var thumbBox = this.thumb.getBoundingClientRect(); + if ( + thumbBox.left <= evt.clientX && evt.clientX <= thumbBox.right && + thumbBox.top <= evt.clientY && evt.clientY <= thumbBox.bottom + ) { + this._bound.onmouseover(evt); + } else { + this._bound.onmouseout(evt); + } + + evt.stopPropagation(); + evt.preventDefault(); + } +}; + +var orientationHashes = { + vertical: { + coordinate: 'clientY', + axis: 'pageY', + size: 'height', + outside: 'right', + inside: 'left', + leading: 'top', + trailing: 'bottom', + marginLeading: 'marginTop', + marginTrailing: 'marginBottom', + thickness: 'width', + delta: 'deltaY' + }, + horizontal: { + coordinate: 'clientX', + axis: 'pageX', + size: 'width', + outside: 'bottom', + inside: 'top', + leading: 'left', + trailing: 'right', + marginLeading: 'marginLeft', + marginTrailing: 'marginRight', + thickness: 'height', + delta: 'deltaX' + } +}; + +var axis = { + top: 'vertical', + bottom: 'vertical', + height: 'vertical', + left: 'horizontal', + right: 'horizontal', + width: 'horizontal' +}; + +var cssFinBars; // definition inserted by gulpfile between following comments +/* inject:css */ +cssFinBars = 'div.finbar-horizontal,div.finbar-vertical{position:absolute;margin:3px}div.finbar-horizontal>.thumb,div.finbar-vertical>.thumb{position:absolute;background-color:#d3d3d3;-webkit-box-shadow:0 0 1px #000;-moz-box-shadow:0 0 1px #000;box-shadow:0 0 1px #000;border-radius:4px;margin:2px;opacity:.4;transition:opacity .5s}div.finbar-horizontal>.thumb.hover,div.finbar-vertical>.thumb.hover{opacity:1;transition:opacity .5s}div.finbar-vertical{top:0;bottom:0;right:0;width:11px}div.finbar-vertical>.thumb{top:0;right:0;width:7px}div.finbar-horizontal{left:0;right:0;bottom:0;height:11px}div.finbar-horizontal>.thumb{left:0;bottom:0;height:7px}'; +/* endinject */ + +function error(msg) { + throw 'finbars: ' + msg; +} + +// Interface +module.exports = FinBar; + +},{"css-injector":2}],5:[function(require,module,exports){ +/* eslint-env browser */ + +'use strict'; + +var rectangular = require('rectangular'); + +var gestures = require('./js/polymergestures.dev.js'); +var GraphicsContext = require('./js/GraphicsContext.js'); + +var RESIZE_POLLING_INTERVAL = 200, + paintables = [], + resizables = [], + paintLoopRunning = true, + resizeLoopRunning = true, + charMap = makeCharMap(); + +function Canvas(div, component) { + var self = this; + + this.div = div; + this._component = component; + + this.dragEndtime = Date.now(); + + this.canvas = document.createElement('canvas'); + this.div.appendChild(this.canvas); + + this.canvas.style.outline = 'none'; + + // this.focuser = document.createElement('button'); + // this.focuser.style.position = 'absolute'; + // this.focuser.style.top = '0px'; + // this.focuser.style.left = '0px'; + // this.focuser.style.zIndex = '-1'; + // this.focuser.style.outline = 'none'; + // this.div.appendChild(this.focuser); + + this.canvasCTX = this.canvas.getContext('2d'); + this.gc = new GraphicsContext(this.canvasCTX); + + this.buffer = document.createElement('canvas'); + this.bufferCTX = this.buffer.getContext('2d'); + this.bufferGC = new GraphicsContext(this.bufferCTX); + + this.mouseLocation = new rectangular.Point(-1, -1); + this.dragstart = new rectangular.Point(-1, -1); + //this.origin = new rectangular.Point(0, 0); + this.bounds = new rectangular.Rectangle(0, 0, 0, 0); + this.hasMouse = false; + + document.addEventListener('mousemove', function(e) { + if (self.hasMouse || self.isDragging()) { + self.finmousemove(e); + } + }); + document.addEventListener('mouseup', function(e) { + self.finmouseup(e); + }); + document.addEventListener('wheel', function(e) { + self.finwheelmoved(e); + }); + document.addEventListener('keydown', function(e) { + self.finkeydown(e); + }); + document.addEventListener('keyup', function(e) { + self.finkeyup(e); + }); + + this.canvas.onmouseover = function() { + self.hasMouse = true; + }; + this.canvas.addEventListener('focus', function(e) { + self.finfocusgained(e); + }); + this.canvas.addEventListener('blur', function(e) { + self.finfocuslost(e); + }); + this.canvas.addEventListener('mousedown', function(e) { + self.finmousedown(e); + }); + this.canvas.addEventListener('mouseout', function(e) { + self.hasMouse = false; + self.finmouseout(e); + }); + this.canvas.addEventListener('click', function(e) { + self.finclick(e); + }); + this.canvas.addEventListener('contextmenu', function(e) { + self.fincontextmenu(e); + e.preventDefault(); + return false; + }); + + gestures.addEventListener(this.canvas, 'tap', function(e) { + self.fintap(e); + }); + gestures.addEventListener(this.canvas, 'holdpulse', function(e) { + self.finholdpulse(e); + }); + gestures.addEventListener(this.canvas, 'flick', function(e) { + self.finflick(e); + }); + gestures.addEventListener(this.canvas, 'release', function(e) { + self.finrelease(e); + }); + gestures.addEventListener(this.canvas, 'trackstart', function(e) { + self.fintrackstart(e); + }); + gestures.addEventListener(this.canvas, 'track', function(e) { + self.fintrack(e); + }); + gestures.addEventListener(this.canvas, 'trackend', function(e) { + self.fintrackend(e); + }); + + this.canvas.setAttribute('tabindex', 0); + this.canvas.contentEditable = true; + + this.resize(); + + this.beginResizing(); + this.beginPainting(); +} + +Canvas.prototype = { + constructor: Canvas.prototype.constructor, + div: null, + _component: null, + gestures: gestures, // TODO: why do we need this? (was previously at bottom of file) + canvas: null, + canvasCTX: null, + focuser: null, + buffer: null, + ctx: null, + mouseLocation: null, + holdPulseCount: -1, + dragstart: null, + origin: null, + bounds: null, + dirty: false, + size: null, + mousedown: false, + dragging: false, + repeatKeyCount: 0, + repeatKey: null, + repeatKeyStartTime: 0, + currentKeys: [], + hasMouse: false, + lastDoubleClickTime: 0, + dragEndTime: 0, + lastRepaintTime: 0, + + addEventListener: function(name, callback) { + this.canvas.addEventListener(name, callback); + }, + + stopPaintLoop: function() { + paintLoopRunning = false; + }, + + restartPaintLoop: function() { + if (paintLoopRunning) { + return; // already running + } + paintLoopRunning = true; + requestAnimationFrame(paintLoopFunction); + }, + + stopResizeLoop: function() { + resizeLoopRunning = false; + }, + + restartResizeLoop: function() { + if (resizeLoopRunning) { + return; // already running + } + resizeLoopRunning = true; + setInterval(resizablesLoopFunction, 200); + }, + + detached: function() { + this.stopPainting(); + this.stopResizing(); + }, + + useHiDPI: function() { + return this._component.resolveProperty('useHiDPI'); + }, + + useBitBlit: function() { + return this._component.resolveProperty('useBitBlit'); + }, + + getFPS: function() { + var fps = this._component.resolveProperty('repaintIntervalRate'); + return fps ? parseInt(fps) : 0; + }, + + tickPaint: function(now) { + var fps = this.getFPS(); + if (fps === 0) { + return; + } + var interval = 1000 / fps; + + var elapsed = now - this.lastRepaintTime; + if (elapsed > interval && this.dirty) { + this.lastRepaintTime = now - (elapsed % interval); + this.paintNow(); + } + }, + + beginPainting: function() { + var self = this; + this.dirty = true; + this.tickPainter = function(now) { + self.tickPaint(now); + }; + paintables.push(this); + }, + + stopPainting: function() { + paintables.splice(paintables.indexOf(this), 1); + }, + + beginResizing: function() { + var self = this; + this.tickResizer = function() { + self.checksize(); + }; + resizables.push(this); + }, + + stopResizing: function() { + resizables.splice(resizables.indexOf(this), 1); + }, + + start: function() { + this.beginPainting(); + this.beginResizing(); + }, + + stop: function() { + this.stopPainting(); + this.stopResizing(); + }, + + checksize: function() { + //this is expensive lets do it at some modulo + var sizeNow = this.div.getBoundingClientRect(); + if (sizeNow.width !== this.size.width || sizeNow.height !== this.size.height) { + this.sizeChangedNotification(); + } + }, + + sizeChangedNotification: function() { + this.resize(); + }, + + resize: function() { + var box = this.size = this.div.getBoundingClientRect(); + + this.canvas.width = this.buffer.width = box.width; + this.canvas.height = this.buffer.height = box.height; + + //fix ala sir spinka, see + //http://www.html5rocks.com/en/tutorials/canvas/hidpi/ + //just add 'hdpi' as an attribute to the fin-canvas tag + var ratio = 1; + var useBitBlit = this.useBitBlit(); + var isHIDPI = window.devicePixelRatio && this.useHiDPI(); + if (isHIDPI) { + var devicePixelRatio = window.devicePixelRatio || 1; + var backingStoreRatio = this.canvasCTX.webkitBackingStorePixelRatio || + this.canvasCTX.mozBackingStorePixelRatio || + this.canvasCTX.msBackingStorePixelRatio || + this.canvasCTX.oBackingStorePixelRatio || + this.canvasCTX.backingStorePixelRatio || 1; + + ratio = devicePixelRatio / backingStoreRatio; + //this.canvasCTX.scale(ratio, ratio); + } + var width = this.canvas.getAttribute('width'); + var height = this.canvas.getAttribute('height'); + this.canvas.width = width * ratio; + this.canvas.height = height * ratio; + this.buffer.width = width * ratio; + this.buffer.height = height * ratio; + + this.canvas.style.width = width + 'px'; + this.canvas.style.height = height + 'px'; + this.buffer.style.width = width + 'px'; + this.buffer.style.height = height + 'px'; + + this.bufferCTX.scale(ratio, ratio); + if (isHIDPI && !useBitBlit) { + this.canvasCTX.scale(ratio, ratio); + } + + //this.origin = new rectangular.Point(Math.round(this.size.left), Math.round(this.size.top)); + this.bounds = new rectangular.Rectangle(0, 0, box.width, box.height); + //setTimeout(function() { + var comp = this._component; + if (comp) { + comp.setBounds(this.bounds); + } + this.resizeNotification(); + this.paintNow(); + //}); + }, + + resizeNotification: function() { + //to be overridden + }, + + getBounds: function() { + return this.bounds; + }, + + paintNow: function() { + var self = this; + this.safePaintImmediately(function(gc) { + gc.clearRect(0, 0, self.canvas.width, self.canvas.height); + + var comp = self._component; + if (comp) { + comp._paint(gc); + } + + self.dirty = false; + }); + }, + + safePaintImmediately: function(paintFunction) { + var useBitBlit = this.useBitBlit(), + gc = useBitBlit ? this.bufferGC : this.gc; + try { + gc.save(); + paintFunction(gc); + } catch (e) { + console.error(e); + } finally { + gc.restore(); + } + if (useBitBlit) { + this.flushBuffer(); + } + }, + + flushBuffer: function() { + if (this.buffer.width > 0 && this.buffer.height > 0) { + this.canvasCTX.drawImage(this.buffer, 0, 0); + } + }, + + dispatchNewEvent: function(event, name, detail) { + detail = { + detail: detail || {} + }; + detail.detail.primitiveEvent = event; + return this.canvas.dispatchEvent(new CustomEvent(name, detail)); + }, + + dispatchNewMouseKeysEvent: function(event, name, detail) { + detail = detail || {}; + detail.mouse = this.mouseLocation; + detail.keys = this.currentKeys; + return this.dispatchNewEvent(event, name, detail); + }, + + finmousemove: function(e) { + if (!this.isDragging() && this.mousedown) { + this.beDragging(); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dragstart', { + isRightClick: this.isRightClick(e) + }); + this.dragstart = new rectangular.Point(this.mouseLocation.x, this.mouseLocation.y); + } + this.mouseLocation = this.getLocal(e); + //console.log(this.mouseLocation); + if (this.isDragging()) { + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-drag', { + dragstart: this.dragstart, + isRightClick: this.isRightClick(e) + }); + } + if (this.bounds.contains(this.mouseLocation)) { + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mousemove'); + } + }, + + finmousedown: function(e) { + this.mouseLocation = this.getLocal(e); + this.mousedown = true; + + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mousedown', { + isRightClick: this.isRightClick(e) + }); + this.takeFocus(); + }, + + finmouseup: function(e) { + if (this.isDragging()) { + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dragend', { + dragstart: this.dragstart, + isRightClick: this.isRightClick(e) + }); + this.beNotDragging(); + this.dragEndtime = Date.now(); + } + this.mousedown = false; + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mouseup', { + isRightClick: this.isRightClick(e) + }); + //this.mouseLocation = new rectangular.Point(-1, -1); + }, + + finmouseout: function(e) { + if (!this.mousedown) { + this.mouseLocation = new rectangular.Point(-1, -1); + } + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mouseout'); + }, + + finwheelmoved: function(e) { + if (this.isDragging() || !this.hasFocus()) { + return; + } + e.preventDefault(); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-wheelmoved', { + isRightClick: this.isRightClick(e) + }); + }, + + finclick: function(e) { + if (Date.now() - this.lastClickTime < 250) { + //this is a double click... + this.findblclick(e); + return; + } + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-click', { + isRightClick: this.isRightClick(e) + }); + this.lastClickTime = Date.now(); + }, + + finrelease: function(e) { + this.holdPulseCount = 0; + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-release'); + }, + + finflick: function(e) { + if (!this.hasFocus()) { + return; + } + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-flick', { + isRightClick: this.isRightClick(e) + }); + }, + + fintrackstart: function(e) { + if (!this.hasFocus()) { + return; + } + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-trackstart'); + }, + + fintrack: function(e) { + if (!this.hasFocus()) { + return; + } + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-track'); + }, + + fintrackend: function(e) { + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-trackend'); + }, + + finhold: function(e) { + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-hold', { + isRightClick: this.isRightClick(e) + }); + }, + + finholdpulse: function(e) { + this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-holdpulse', { + count: this.holdPulseCount++ + }); + }, + + fintap: function(e) { + //this nonsense is to hold a tap if it's really a double click + var self = this; + var now = Date.now(); + var dif = now - this.lastDoubleClickTime; + if (dif < 300) { + return; + } + //dragend is also causing a tap + //lets fix this here + if (now - this.dragEndtime < 100) { + return; + } + setTimeout(function() { + self._fintap(e); + }, 180); + }, + + _fintap: function(e) { + //this nonsense is to hold a tap if it's really a double click + var now = Date.now(); + var dif = now - this.lastDoubleClickTime; + if (dif < 300) { + return; + } + //this.mouseLocation = this.getLocal(e); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-tap', { + isRightClick: this.isRightClick(e) + }); + }, + + findblclick: function(e) { + this.mouseLocation = this.getLocal(e); + this.lastDoubleClickTime = Date.now(); + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dblclick', { + isRightClick: this.isRightClick(e) + }); + //console.log('dblclick', this.currentKeys); + }, + + getCharMap: function() { //TODO: This is static. Make it a property of the constructor. + return charMap; + }, + + finkeydown: function(e) { + if (!this.hasFocus()) { + return; + } + + //e.preventDefault(); + var keyChar = e.shiftKey ? charMap[e.keyCode][1] : charMap[e.keyCode][0]; + if (e.repeat) { + if (this.repeatKey === keyChar) { + this.repeatKeyCount++; + } else { + this.repeatKey = keyChar; + this.repeatKeyStartTime = Date.now(); + } + } else { + this.repeatKey = null; + this.repeatKeyCount = 0; + this.repeatKeyStartTime = 0; + } + if (this.currentKeys.indexOf(keyChar) === -1) { + this.currentKeys.push(keyChar); + } + //console.log(keyChar, e.keyCode); + this.dispatchNewEvent(e, 'fin-canvas-keydown', { + alt: e.altKey, + ctrl: e.ctrlKey, + char: keyChar, + code: e.charCode, + key: e.keyCode, + meta: e.metaKey, + repeatCount: this.repeatKeyCount, + repeatStartTime: this.repeatKeyStartTime, + shift: e.shiftKey, + identifier: e.keyIdentifier, + currentKeys: this.currentKeys.slice(0) + }); + }, + + finkeyup: function(e) { + var keyChar = e.shiftKey ? charMap[e.keyCode][1] : charMap[e.keyCode][0]; + this.currentKeys.splice(this.currentKeys.indexOf(keyChar), 1); + if (!this.hasFocus()) { + return; + } + this.repeatKeyCount = 0; + this.repeatKey = null; + this.repeatKeyStartTime = 0; + this.dispatchNewEvent(e, 'fin-canvas-keyup', { + alt: e.altKey, + ctrl: e.ctrlKey, + char: keyChar, + code: e.charCode, + key: e.keyCode, + meta: e.metaKey, + repeat: e.repeat, + shift: e.shiftKey, + identifier: e.keyIdentifier, + currentKeys: this.currentKeys.slice(0) + }); + }, + + finfocusgained: function(e) { + this.dispatchNewEvent(e, 'fin-canvas-focus-gained'); + }, + + finfocuslost: function(e) { + this.dispatchNewEvent(e, 'fin-canvas-focus-lost'); + }, + + fincontextmenu: function(e) { + if (e.ctrlKey && this.currentKeys.indexOf('CTRL') === -1) { + this.currentKeys.push('CTRL'); + } + if (Date.now() - this.lastClickTime < 250) { + //this is a double click... + this.findblclick(e); + return; + } + this.dispatchNewMouseKeysEvent(e, 'fin-canvas-context-menu', { + isRightClick: this.isRightClick(e) + }); + this.lastClickTime = Date.now(); + }, + + repaint: function() { + var fps = this.getFPS(); + this.dirty = true; + if (!paintLoopRunning || fps === 0) { + this.paintNow(); + } + }, + + getMouseLocation: function() { + return this.mouseLocation; + }, + + getOrigin: function() { + var rect = this.canvas.getBoundingClientRect(); + var p = new rectangular.Point(rect.left, rect.top); + return p; + }, + + getLocal: function(e) { + var rect = this.canvas.getBoundingClientRect(); + var p = new rectangular.Point(e.clientX - rect.left, e.clientY - rect.top); + return p; + }, + + hasFocus: function() { + return document.activeElement === this.canvas; + }, + + takeFocus: function() { + var self = this; + if (!this.hasFocus()) { + setTimeout(function() { + self.canvas.focus(); + }, 10); + } + }, + + beDragging: function() { + this.dragging = true; + this.disableDocumentElementSelection(); + }, + + beNotDragging: function() { + this.dragging = false; + this.enableDocumentElementSelection(); + }, + + isDragging: function() { + return this.dragging; + }, + + disableDocumentElementSelection: function() { + var style = document.body.style; + style.cssText = style.cssText + '-webkit-user-select: none'; + }, + + enableDocumentElementSelection: function() { + var style = document.body.style; + style.cssText = style.cssText.replace('-webkit-user-select: none', ''); + }, + + setFocusable: function(truthy) { + this.focuser.style.display = truthy ? '' : 'none'; + }, + + isRightClick: function(e) { + var isRightMB; + e = e || window.event; + + if ('which' in e) { // Gecko (Firefox), WebKit (Safari/Chrome) & Opera + isRightMB = e.which === 3; + } else if ('button' in e) { // IE, Opera + isRightMB = e.button === 2; + } + return isRightMB; + }, + + dispatchEvent: function(e) { + return this.canvas.dispatchEvent(e); + } +}; + +function paintLoopFunction(now) { + if (!paintLoopRunning) { + return; + } + for (var i = 0; i < paintables.length; i++) { + try { + paintables[i].tickPainter(now); + } catch (e) { + console.error(e); + } + } + requestAnimationFrame(paintLoopFunction); +} +requestAnimationFrame(paintLoopFunction); + +function resizablesLoopFunction(now) { + if (!resizeLoopRunning) { + return; + } + for (var i = 0; i < resizables.length; i++) { + try { + resizables[i].tickResizer(now); + } catch (e) { + console.error(e); + } + } +} +setInterval(resizablesLoopFunction, RESIZE_POLLING_INTERVAL); + +function makeCharMap() { + var map = []; + + var empty = ['', '']; + + for (var i = 0; i < 256; i++) { + map[i] = empty; + } + + map[27] = ['ESC', 'ESCSHIFT']; + map[192] = ['`', '~']; + map[49] = ['1', '!']; + map[50] = ['2', '@']; + map[51] = ['3', '#']; + map[52] = ['4', '$']; + map[53] = ['5', '%']; + map[54] = ['6', '^']; + map[55] = ['7', '&']; + map[56] = ['8', '*']; + map[57] = ['9', '(']; + map[48] = ['0', ')']; + map[189] = ['-', '_']; + map[187] = ['=', '+']; + map[8] = ['DELETE', 'DELETESHIFT']; + map[9] = ['TAB', 'TABSHIFT']; + map[81] = ['q', 'Q']; + map[87] = ['w', 'W']; + map[69] = ['e', 'E']; + map[82] = ['r', 'R']; + map[84] = ['t', 'T']; + map[89] = ['y', 'Y']; + map[85] = ['u', 'U']; + map[73] = ['i', 'I']; + map[79] = ['o', 'O']; + map[80] = ['p', 'P']; + map[219] = ['[', '{']; + map[221] = [']', '}']; + map[220] = ['\\', '|']; + map[220] = ['CAPSLOCK', 'CAPSLOCKSHIFT']; + map[65] = ['a', 'A']; + map[83] = ['s', 'S']; + map[68] = ['d', 'D']; + map[70] = ['f', 'F']; + map[71] = ['g', 'G']; + map[72] = ['h', 'H']; + map[74] = ['j', 'J']; + map[75] = ['k', 'K']; + map[76] = ['l', 'L']; + map[186] = [';', ':']; + map[222] = ['\'', '|']; + map[13] = ['RETURN', 'RETURNSHIFT']; + map[16] = ['SHIFT', 'SHIFT']; + map[90] = ['z', 'Z']; + map[88] = ['x', 'X']; + map[67] = ['c', 'C']; + map[86] = ['v', 'V']; + map[66] = ['b', 'B']; + map[78] = ['n', 'N']; + map[77] = ['m', 'M']; + map[188] = [',', '<']; + map[190] = ['.', '>']; + map[191] = ['/', '?']; + map[16] = ['SHIFT', 'SHIFT']; + map[17] = ['CTRL', 'CTRLSHIFT']; + map[18] = ['ALT', 'ALTSHIFT']; + map[91] = ['COMMANDLEFT', 'COMMANDLEFTSHIFT']; + map[32] = ['SPACE', 'SPACESHIFT']; + map[93] = ['COMMANDRIGHT', 'COMMANDRIGHTSHIFT']; + map[18] = ['ALT', 'ALTSHIFT']; + map[38] = ['UP', 'UPSHIFT']; + map[37] = ['LEFT', 'LEFTSHIFT']; + map[40] = ['DOWN', 'DOWNSHIFT']; + map[39] = ['RIGHT', 'RIGHTSHIFT']; + + map[33] = ['PAGEUP', 'PAGEUPSHIFT']; + map[34] = ['PAGEDOWN', 'PAGEDOWNSHIFT']; + map[35] = ['PAGERIGHT', 'PAGERIGHTSHIFT']; + map[36] = ['PAGELEFT', 'PAGELEFTSHIFT']; + + return map; +} + +module.exports = Canvas; + +},{"./js/GraphicsContext.js":6,"./js/polymergestures.dev.js":8,"rectangular":29}],6:[function(require,module,exports){ +'use strict'; + +var consoleLogger = require('./gc-console-logger'); + +/** + * @constructor + * @param gc - The 2-D graphics context from your canvas + * @param {boolean|apiLogger} [logger=true] + * * `true` uses `gc-console-logger` function bound to 'gc.' as prefix + * * string uses `gc-console-logger` function bound to string + * * function used as is + */ +function GraphicsContext(gc, logger) { + this.gc = gc; + + var self = this; + var reWEBKIT = /^webkit/; + + switch (typeof logger) { + + case 'string': + logger = consoleLogger.bind(undefined, logger + '.'); + break; + + case 'boolean': + if (logger === true) { + logger = consoleLogger.bind(undefined, 'gc.'); + } + break; + + case 'function': + if (logger.length !== 3) { + throw 'GraphicsContext: User-supplied API logger function does not accept three parameters.'; + } + break; + + default: + logger = false; + } + + // Stub out all the prototype members of the canvas 2D graphics context: + Object.keys(Object.getPrototypeOf(gc)).forEach(MakeStub); + + // Some older browsers (e.g., Chrome 40) did not have all members of canvas + // 2D graphics context in the prototype so we make this additional call: + Object.keys(gc).forEach(MakeStub); + + function MakeStub(key) { + if (key in GraphicsContext.prototype || reWEBKIT.test(key)) { + return; + } + if (typeof gc[key] === 'function') { + self[key] = !logger ? gc[key].bind(gc) : function() { + return logger(key, arguments, gc[key].apply(gc, arguments)); + }; + } else { + Object.defineProperty(self, key, { + get: function() { + var result = gc[key]; + return logger ? logger(key, 'getter', result) : result; + }, + set: function(value) { + gc[key] = logger ? logger(key, 'setter', value) : value; + } + }); + } + } +} + +module.exports = GraphicsContext; + +},{"./gc-console-logger":7}],7:[function(require,module,exports){ +'use strict'; + +var YIELDS = '\u27F9'; // LONG RIGHTWARDS DOUBLE ARROW + +function consoleLogger(prefix, name, args, value) { + var result = value; + + if (typeof value === 'string') { + result = '"' + result + '"'; + } + + name = prefix + name; + + switch (args) { + case 'getter': + console.log(name, '=', result); + break; + + case 'setter': + console.log(name, YIELDS, result); + break; + + default: // method call + name += '(' + Array.prototype.slice.call(args).join(', ') + ')'; + if (result === undefined) { + console.log(name); + } else { + console.log(name, YIELDS, result); + } + } + + return value; +} + +module.exports = consoleLogger; + +},{}],8:[function(require,module,exports){ +/* eslint-disable */ + +/** + * @license + * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. + * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt + * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt + * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt + * Code distributed by Google as part of the polymer project is also + * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt + */ +//module.exports = {}; + +(function(scope) { + var hasFullPath = false; + + // test for full event path support + var pathTest = document.createElement('meta'); + if (pathTest.createShadowRoot) { + var sr = pathTest.createShadowRoot(); + var s = document.createElement('span'); + sr.appendChild(s); + pathTest.addEventListener('testpath', function(ev) { + if (ev.path) { + // if the span is in the event path, then path[0] is the real source for all events + hasFullPath = ev.path[0] === s; + } + ev.stopPropagation(); + }); + var ev = new CustomEvent('testpath', { + bubbles: true + }); + // must add node to DOM to trigger event listener + document.head.appendChild(pathTest); + s.dispatchEvent(ev); + pathTest.parentNode.removeChild(pathTest); + sr = s = null; + } + pathTest = null; + + var target = { + shadow: function(inEl) { + if (inEl) { + return inEl.shadowRoot || inEl.webkitShadowRoot; + } + }, + canTarget: function(shadow) { + return shadow && Boolean(shadow.elementFromPoint); + }, + targetingShadow: function(inEl) { + var s = this.shadow(inEl); + if (this.canTarget(s)) { + return s; + } + }, + olderShadow: function(shadow) { + var os = shadow.olderShadowRoot; + if (!os) { + var se = shadow.querySelector('shadow'); + if (se) { + os = se.olderShadowRoot; + } + } + return os; + }, + allShadows: function(element) { + var shadows = [], + s = this.shadow(element); + while (s) { + shadows.push(s); + s = this.olderShadow(s); + } + return shadows; + }, + searchRoot: function(inRoot, x, y) { + var t, st, sr, os; + if (inRoot) { + t = inRoot.elementFromPoint(x, y); + if (t) { + // found element, check if it has a ShadowRoot + sr = this.targetingShadow(t); + } else if (inRoot !== document) { + // check for sibling roots + sr = this.olderShadow(inRoot); + } + // search other roots, fall back to light dom element + return this.searchRoot(sr, x, y) || t; + } + }, + owner: function(element) { + if (!element) { + return document; + } + var s = element; + // walk up until you hit the shadow root or document + while (s.parentNode) { + s = s.parentNode; + } + // the owner element is expected to be a Document or ShadowRoot + if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) { + s = document; + } + return s; + }, + findTarget: function(inEvent) { + if (hasFullPath && inEvent.path && inEvent.path.length) { + return inEvent.path[0]; + } + var x = inEvent.clientX, + y = inEvent.clientY; + // if the listener is in the shadow root, it is much faster to start there + var s = this.owner(inEvent.target); + // if x, y is not in this root, fall back to document search + if (!s.elementFromPoint(x, y)) { + s = document; + } + return this.searchRoot(s, x, y); + }, + findTouchAction: function(inEvent) { + var n; + if (hasFullPath && inEvent.path && inEvent.path.length) { + var path = inEvent.path; + for (var i = 0; i < path.length; i++) { + n = path[i]; + if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { + return n.getAttribute('touch-action'); + } + } + } else { + n = inEvent.target; + while (n) { + if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) { + return n.getAttribute('touch-action'); + } + n = n.parentNode || n.host; + } + } + // auto is default + return "auto"; + }, + LCA: function(a, b) { + if (a === b) { + return a; + } + if (a && !b) { + return a; + } + if (b && !a) { + return b; + } + if (!b && !a) { + return document; + } + // fast case, a is a direct descendant of b or vice versa + if (a.contains && a.contains(b)) { + return a; + } + if (b.contains && b.contains(a)) { + return b; + } + var adepth = this.depth(a); + var bdepth = this.depth(b); + var d = adepth - bdepth; + if (d >= 0) { + a = this.walk(a, d); + } else { + b = this.walk(b, -d); + } + while (a && b && a !== b) { + a = a.parentNode || a.host; + b = b.parentNode || b.host; + } + return a; + }, + walk: function(n, u) { + for (var i = 0; n && (i < u); i++) { + n = n.parentNode || n.host; + } + return n; + }, + depth: function(n) { + var d = 0; + while (n) { + d++; + n = n.parentNode || n.host; + } + return d; + }, + deepContains: function(a, b) { + var common = this.LCA(a, b); + // if a is the common ancestor, it must "deeply" contain b + return common === a; + }, + insideNode: function(node, x, y) { + var rect = node.getBoundingClientRect(); + return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom); + }, + path: function(event) { + var p; + if (hasFullPath && event.path && event.path.length) { + p = event.path; + } else { + p = []; + var n = this.findTarget(event); + while (n) { + p.push(n); + n = n.parentNode || n.host; + } + } + return p; + } + }; + scope.targetFinding = target; + /** + * Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting + * + * @param {Event} Event An event object with clientX and clientY properties + * @return {Element} The probable event origninator + */ + scope.findTarget = target.findTarget.bind(target); + /** + * Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM + * roots. + * + * @param {Node} container + * @param {Node} containee + * @return {Boolean} + */ + scope.deepContains = target.deepContains.bind(target); + + /** + * Determines if the x/y position is inside the given node. + * + * Example: + * + * function upHandler(event) { + * var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY); + * if (innode) { + * // wait for tap? + * } else { + * // tap will never happen + * } + * } + * + * @param {Node} node + * @param {Number} x Screen X position + * @param {Number} y screen Y position + * @return {Boolean} + */ + scope.insideNode = target.insideNode; + +})(exports); + +(function() { + function shadowSelector(v) { + return 'html /deep/ ' + selector(v); + } + + function selector(v) { + return '[touch-action="' + v + '"]'; + } + + function rule(v) { + return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + ';}'; + } + var attrib2css = [ + 'none', + 'auto', + 'pan-x', + 'pan-y', { + rule: 'pan-x pan-y', + selectors: [ + 'pan-x pan-y', + 'pan-y pan-x' + ] + }, + 'manipulation' + ]; + var styles = ''; + // only install stylesheet if the browser has touch action support + var hasTouchAction = typeof document.head.style.touchAction === 'string'; + // only add shadow selectors if shadowdom is supported + var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot; + + if (hasTouchAction) { + attrib2css.forEach(function(r) { + if (String(r) === r) { + styles += selector(r) + rule(r) + '\n'; + if (hasShadowRoot) { + styles += shadowSelector(r) + rule(r) + '\n'; + } + } else { + styles += r.selectors.map(selector) + rule(r.rule) + '\n'; + if (hasShadowRoot) { + styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n'; + } + } + }); + + var el = document.createElement('style'); + el.textContent = styles; + document.head.appendChild(el); + } +})(); + +/** + * This is the constructor for new PointerEvents. + * + * New Pointer Events must be given a type, and an optional dictionary of + * initialization properties. + * + * Due to certain platform requirements, events returned from the constructor + * identify as MouseEvents. + * + * @constructor + * @param {String} inType The type of the event to create. + * @param {Object} [inDict] An optional dictionary of initial event properties. + * @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`. + */ +(function(scope) { + + var MOUSE_PROPS = [ + 'bubbles', + 'cancelable', + 'view', + 'detail', + 'screenX', + 'screenY', + 'clientX', + 'clientY', + 'ctrlKey', + 'altKey', + 'shiftKey', + 'metaKey', + 'button', + 'relatedTarget', + 'pageX', + 'pageY' + ]; + + var MOUSE_DEFAULTS = [ + false, + false, + null, + null, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null, + 0, + 0 + ]; + + var NOP_FACTORY = function() { + return function() {}; + }; + + var eventFactory = { + // TODO(dfreedm): this is overridden by tap recognizer, needs review + preventTap: NOP_FACTORY, + makeBaseEvent: function(inType, inDict) { + var e = document.createEvent('Event'); + e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false); + e.preventTap = eventFactory.preventTap(e); + return e; + }, + makeGestureEvent: function(inType, inDict) { + inDict = inDict || Object.create(null); + + var e = this.makeBaseEvent(inType, inDict); + for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) { + k = keys[i]; + if (k !== 'bubbles' && k !== 'cancelable') { + e[k] = inDict[k]; + } + } + return e; + }, + makePointerEvent: function(inType, inDict) { + inDict = inDict || Object.create(null); + + var e = this.makeBaseEvent(inType, inDict); + // define inherited MouseEvent properties + for (var i = 2, p; i < MOUSE_PROPS.length; i++) { + p = MOUSE_PROPS[i]; + e[p] = inDict[p] || MOUSE_DEFAULTS[i]; + } + e.buttons = inDict.buttons || 0; + + // Spec requires that pointers without pressure specified use 0.5 for down + // state and 0 for up state. + var pressure = 0; + if (inDict.pressure) { + pressure = inDict.pressure; + } else { + pressure = e.buttons ? 0.5 : 0; + } + + // add x/y properties aliased to clientX/Y + e.x = e.clientX; + e.y = e.clientY; + + // define the properties of the PointerEvent interface + e.pointerId = inDict.pointerId || 0; + e.width = inDict.width || 0; + e.height = inDict.height || 0; + e.pressure = pressure; + e.tiltX = inDict.tiltX || 0; + e.tiltY = inDict.tiltY || 0; + e.pointerType = inDict.pointerType || ''; + e.hwTimestamp = inDict.hwTimestamp || 0; + e.isPrimary = inDict.isPrimary || false; + e._source = inDict._source || ''; + return e; + } + }; + + scope.eventFactory = eventFactory; +})(exports); + +/** + * This module implements an map of pointer states + */ +(function(scope) { + var USE_MAP = window.Map && window.Map.prototype.forEach; + var POINTERS_FN = function() { + return this.size; + }; + + function PointerMap() { + if (USE_MAP) { + var m = new Map(); + m.pointers = POINTERS_FN; + return m; + } else { + this.keys = []; + this.values = []; + } + } + + PointerMap.prototype = { + set: function(inId, inEvent) { + var i = this.keys.indexOf(inId); + if (i > -1) { + this.values[i] = inEvent; + } else { + this.keys.push(inId); + this.values.push(inEvent); + } + }, + has: function(inId) { + return this.keys.indexOf(inId) > -1; + }, + 'delete': function(inId) { + var i = this.keys.indexOf(inId); + if (i > -1) { + this.keys.splice(i, 1); + this.values.splice(i, 1); + } + }, + get: function(inId) { + var i = this.keys.indexOf(inId); + return this.values[i]; + }, + clear: function() { + this.keys.length = 0; + this.values.length = 0; + }, + // return value, key, map + forEach: function(callback, thisArg) { + this.values.forEach(function(v, i) { + callback.call(thisArg, v, this.keys[i], this); + }, this); + }, + pointers: function() { + return this.keys.length; + } + }; + + scope.PointerMap = PointerMap; +})(exports); + +(function(scope) { + var CLONE_PROPS = [ + // MouseEvent + 'bubbles', + 'cancelable', + 'view', + 'detail', + 'screenX', + 'screenY', + 'clientX', + 'clientY', + 'ctrlKey', + 'altKey', + 'shiftKey', + 'metaKey', + 'button', + 'relatedTarget', + // DOM Level 3 + 'buttons', + // PointerEvent + 'pointerId', + 'width', + 'height', + 'pressure', + 'tiltX', + 'tiltY', + 'pointerType', + 'hwTimestamp', + 'isPrimary', + // event instance + 'type', + 'target', + 'currentTarget', + 'which', + 'pageX', + 'pageY', + 'timeStamp', + // gesture addons + 'preventTap', + 'tapPrevented', + '_source' + ]; + + var CLONE_DEFAULTS = [ + // MouseEvent + false, + false, + null, + null, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null, + // DOM Level 3 + 0, + // PointerEvent + 0, + 0, + 0, + 0, + 0, + 0, + '', + 0, + false, + // event instance + '', + null, + null, + 0, + 0, + 0, + 0, + function() {}, + false + ]; + + var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined'); + + var eventFactory = scope.eventFactory; + + // set of recognizers to run for the currently handled event + var currentGestures; + + /** + * This module is for normalizing events. Mouse and Touch events will be + * collected here, and fire PointerEvents that have the same semantics, no + * matter the source. + * Events fired: + * - pointerdown: a pointing is added + * - pointerup: a pointer is removed + * - pointermove: a pointer is moved + * - pointerover: a pointer crosses into an element + * - pointerout: a pointer leaves an element + * - pointercancel: a pointer will no longer generate events + */ + var dispatcher = { + IS_IOS: false, + pointermap: new scope.PointerMap(), + requiredGestures: new scope.PointerMap(), + eventMap: Object.create(null), + // Scope objects for native events. + // This exists for ease of testing. + eventSources: Object.create(null), + eventSourceList: [], + gestures: [], + // map gesture event -> {listeners: int, index: gestures[int]} + dependencyMap: { + // make sure down and up are in the map to trigger "register" + down: { + listeners: 0, + index: -1 + }, + up: { + listeners: 0, + index: -1 + } + }, + gestureQueue: [], + /** + * Add a new event source that will generate pointer events. + * + * `inSource` must contain an array of event names named `events`, and + * functions with the names specified in the `events` array. + * @param {string} name A name for the event source + * @param {Object} source A new source of platform events. + */ + registerSource: function(name, source) { + var s = source; + var newEvents = s.events; + if (newEvents) { + newEvents.forEach(function(e) { + if (s[e]) { + this.eventMap[e] = s[e].bind(s); + } + }, this); + this.eventSources[name] = s; + this.eventSourceList.push(s); + } + }, + registerGesture: function(name, source) { + var obj = Object.create(null); + obj.listeners = 0; + obj.index = this.gestures.length; + for (var i = 0, g; i < source.exposes.length; i++) { + g = source.exposes[i].toLowerCase(); + this.dependencyMap[g] = obj; + } + this.gestures.push(source); + }, + register: function(element, initial) { + var l = this.eventSourceList.length; + for (var i = 0, es; + (i < l) && (es = this.eventSourceList[i]); i++) { + // call eventsource register + es.register.call(es, element, initial); + } + }, + unregister: function(element) { + var l = this.eventSourceList.length; + for (var i = 0, es; + (i < l) && (es = this.eventSourceList[i]); i++) { + // call eventsource register + es.unregister.call(es, element); + } + }, + // EVENTS + down: function(inEvent) { + this.requiredGestures.set(inEvent.pointerId, currentGestures); + this.fireEvent('down', inEvent); + }, + move: function(inEvent) { + // pipe move events into gesture queue directly + inEvent.type = 'move'; + this.fillGestureQueue(inEvent); + }, + up: function(inEvent) { + this.fireEvent('up', inEvent); + this.requiredGestures.delete(inEvent.pointerId); + }, + cancel: function(inEvent) { + inEvent.tapPrevented = true; + this.fireEvent('up', inEvent); + this.requiredGestures.delete(inEvent.pointerId); + }, + addGestureDependency: function(node, currentGestures) { + var gesturesWanted = node._pgEvents; + if (gesturesWanted && currentGestures) { + var gk = Object.keys(gesturesWanted); + for (var i = 0, r, ri, g; i < gk.length; i++) { + // gesture + g = gk[i]; + if (gesturesWanted[g] > 0) { + // lookup gesture recognizer + r = this.dependencyMap[g]; + // recognizer index + ri = r ? r.index : -1; + currentGestures[ri] = true; + } + } + } + }, + // LISTENER LOGIC + eventHandler: function(inEvent) { + // This is used to prevent multiple dispatch of events from + // platform events. This can happen when two elements in different scopes + // are set up to create pointer events, which is relevant to Shadow DOM. + + var type = inEvent.type; + + // only generate the list of desired events on "down" + if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') { + if (!inEvent._handledByPG) { + currentGestures = {}; + } + + // in IOS mode, there is only a listener on the document, so this is not re-entrant + if (this.IS_IOS) { + var ev = inEvent; + if (type === 'touchstart') { + var ct = inEvent.changedTouches[0]; + // set up a fake event to give to the path builder + ev = { + target: inEvent.target, + clientX: ct.clientX, + clientY: ct.clientY, + path: inEvent.path + }; + } + // use event path if available, otherwise build a path from target finding + var nodes = inEvent.path || scope.targetFinding.path(ev); + for (var i = 0, n; i < nodes.length; i++) { + n = nodes[i]; + this.addGestureDependency(n, currentGestures); + } + } else { + this.addGestureDependency(inEvent.currentTarget, currentGestures); + } + } + + if (inEvent._handledByPG) { + return; + } + var fn = this.eventMap && this.eventMap[type]; + if (fn) { + fn(inEvent); + } + inEvent._handledByPG = true; + }, + // set up event listeners + listen: function(target, events) { + for (var i = 0, l = events.length, e; + (i < l) && (e = events[i]); i++) { + this.addEvent(target, e); + } + }, + // remove event listeners + unlisten: function(target, events) { + for (var i = 0, l = events.length, e; + (i < l) && (e = events[i]); i++) { + this.removeEvent(target, e); + } + }, + addEvent: function(target, eventName) { + target.addEventListener(eventName, this.boundHandler); + }, + removeEvent: function(target, eventName) { + target.removeEventListener(eventName, this.boundHandler); + }, + // EVENT CREATION AND TRACKING + /** + * Creates a new Event of type `inType`, based on the information in + * `inEvent`. + * + * @param {string} inType A string representing the type of event to create + * @param {Event} inEvent A platform event with a target + * @return {Event} A PointerEvent of type `inType` + */ + makeEvent: function(inType, inEvent) { + var e = eventFactory.makePointerEvent(inType, inEvent); + e.preventDefault = inEvent.preventDefault; + e.tapPrevented = inEvent.tapPrevented; + e._target = e._target || inEvent.target; + return e; + }, + // make and dispatch an event in one call + fireEvent: function(inType, inEvent) { + var e = this.makeEvent(inType, inEvent); + return this.dispatchEvent(e); + }, + /** + * Returns a snapshot of inEvent, with writable properties. + * + * @param {Event} inEvent An event that contains properties to copy. + * @return {Object} An object containing shallow copies of `inEvent`'s + * properties. + */ + cloneEvent: function(inEvent) { + var eventCopy = Object.create(null), + p; + for (var i = 0; i < CLONE_PROPS.length; i++) { + p = CLONE_PROPS[i]; + eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i]; + // Work around SVGInstanceElement shadow tree + // Return the element that is represented by the instance for Safari, Chrome, IE. + // This is the behavior implemented by Firefox. + if (p === 'target' || p === 'relatedTarget') { + if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) { + eventCopy[p] = eventCopy[p].correspondingUseElement; + } + } + } + // keep the semantics of preventDefault + eventCopy.preventDefault = function() { + inEvent.preventDefault(); + }; + return eventCopy; + }, + /** + * Dispatches the event to its target. + * + * @param {Event} inEvent The event to be dispatched. + * @return {Boolean} True if an event handler returns true, false otherwise. + */ + dispatchEvent: function(inEvent) { + var t = inEvent._target; + if (t) { + t.dispatchEvent(inEvent); + // clone the event for the gesture system to process + // clone after dispatch to pick up gesture prevention code + var clone = this.cloneEvent(inEvent); + clone.target = t; + this.fillGestureQueue(clone); + } + }, + gestureTrigger: function() { + // process the gesture queue + for (var i = 0, e, rg; i < this.gestureQueue.length; i++) { + e = this.gestureQueue[i]; + rg = e._requiredGestures; + if (rg) { + for (var j = 0, g, fn; j < this.gestures.length; j++) { + // only run recognizer if an element in the source event's path is listening for those gestures + if (rg[j]) { + g = this.gestures[j]; + fn = g[e.type]; + if (fn) { + fn.call(g, e); + } + } + } + } + } + this.gestureQueue.length = 0; + }, + fillGestureQueue: function(ev) { + // only trigger the gesture queue once + if (!this.gestureQueue.length) { + requestAnimationFrame(this.boundGestureTrigger); + } + ev._requiredGestures = this.requiredGestures.get(ev.pointerId); + this.gestureQueue.push(ev); + } + }; + dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher); + dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher); + scope.dispatcher = dispatcher; + + /** + * Listen for `gesture` on `node` with the `handler` function + * + * If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled. + * + * @param {Element} node + * @param {string} gesture + * @return Boolean `gesture` is a valid gesture + */ + scope.activateGesture = function(node, gesture) { + var g = gesture.toLowerCase(); + var dep = dispatcher.dependencyMap[g]; + if (dep) { + var recognizer = dispatcher.gestures[dep.index]; + if (!node._pgListeners) { + dispatcher.register(node); + node._pgListeners = 0; + } + // TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes + if (recognizer) { + var touchAction = recognizer.defaultActions && recognizer.defaultActions[g]; + var actionNode; + switch (node.nodeType) { + case Node.ELEMENT_NODE: + actionNode = node; + break; + case Node.DOCUMENT_FRAGMENT_NODE: + actionNode = node.host; + break; + default: + actionNode = null; + break; + } + if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) { + actionNode.setAttribute('touch-action', touchAction); + } + } + if (!node._pgEvents) { + node._pgEvents = {}; + } + node._pgEvents[g] = (node._pgEvents[g] || 0) + 1; + node._pgListeners++; + } + return Boolean(dep); + }; + + /** + * + * Listen for `gesture` from `node` with `handler` function. + * + * @param {Element} node + * @param {string} gesture + * @param {Function} handler + * @param {Boolean} capture + */ + scope.addEventListener = function(node, gesture, handler, capture) { + if (handler) { + scope.activateGesture(node, gesture); + node.addEventListener(gesture, handler, capture); + } + }; + + /** + * Tears down the gesture configuration for `node` + * + * If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled. + * + * @param {Element} node + * @param {string} gesture + * @return Boolean `gesture` is a valid gesture + */ + scope.deactivateGesture = function(node, gesture) { + var g = gesture.toLowerCase(); + var dep = dispatcher.dependencyMap[g]; + if (dep) { + if (node._pgListeners > 0) { + node._pgListeners--; + } + if (node._pgListeners === 0) { + dispatcher.unregister(node); + } + if (node._pgEvents) { + if (node._pgEvents[g] > 0) { + node._pgEvents[g]--; + } else { + node._pgEvents[g] = 0; + } + } + } + return Boolean(dep); + }; + + /** + * Stop listening for `gesture` from `node` with `handler` function. + * + * @param {Element} node + * @param {string} gesture + * @param {Function} handler + * @param {Boolean} capture + */ + scope.removeEventListener = function(node, gesture, handler, capture) { + if (handler) { + scope.deactivateGesture(node, gesture); + node.removeEventListener(gesture, handler, capture); + } + }; +})(exports); + +(function(scope) { + var dispatcher = scope.dispatcher; + var pointermap = dispatcher.pointermap; + // radius around touchend that swallows mouse events + var DEDUP_DIST = 25; + + var WHICH_TO_BUTTONS = [0, 1, 4, 2]; + + var currentButtons = 0; + + var FIREFOX_LINUX = /Linux.*Firefox\//i; + + var HAS_BUTTONS = (function() { + // firefox on linux returns spec-incorrect values for mouseup.buttons + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also + // https://codereview.chromium.org/727593003/#msg16 + if (FIREFOX_LINUX.test(navigator.userAgent)) { + return false; + } + try { + return new MouseEvent('test', { + buttons: 1 + }).buttons === 1; + } catch (e) { + return false; + } + })(); + + // handler block for native mouse events + var mouseEvents = { + POINTER_ID: 1, + POINTER_TYPE: 'mouse', + events: [ + 'mousedown', + 'mousemove', + 'mouseup' + ], + exposes: [ + 'down', + 'up', + 'move' + ], + register: function(target) { + dispatcher.listen(target, this.events); + }, + unregister: function(target) { + if (target.nodeType === Node.DOCUMENT_NODE) { + return; + } + dispatcher.unlisten(target, this.events); + }, + lastTouches: [], + // collide with the global mouse listener + isEventSimulatedFromTouch: function(inEvent) { + var lts = this.lastTouches; + var x = inEvent.clientX, + y = inEvent.clientY; + for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) { + // simulated mouse events will be swallowed near a primary touchend + var dx = Math.abs(x - t.x), + dy = Math.abs(y - t.y); + if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) { + return true; + } + } + }, + prepareEvent: function(inEvent) { + var e = dispatcher.cloneEvent(inEvent); + e.pointerId = this.POINTER_ID; + e.isPrimary = true; + e.pointerType = this.POINTER_TYPE; + e._source = 'mouse'; + if (!HAS_BUTTONS) { + var type = inEvent.type; + var bit = WHICH_TO_BUTTONS[inEvent.which] || 0; + if (type === 'mousedown') { + currentButtons |= bit; + } else if (type === 'mouseup') { + currentButtons &= ~bit; + } + e.buttons = currentButtons; + } + return e; + }, + mousedown: function(inEvent) { + if (!this.isEventSimulatedFromTouch(inEvent)) { + var p = pointermap.has(this.POINTER_ID); + var e = this.prepareEvent(inEvent); + e.target = scope.findTarget(inEvent); + pointermap.set(this.POINTER_ID, e.target); + dispatcher.down(e); + } + }, + mousemove: function(inEvent) { + if (!this.isEventSimulatedFromTouch(inEvent)) { + var target = pointermap.get(this.POINTER_ID); + if (target) { + var e = this.prepareEvent(inEvent); + e.target = target; + // handle case where we missed a mouseup + if ((HAS_BUTTONS ? e.buttons : e.which) === 0) { + if (!HAS_BUTTONS) { + currentButtons = e.buttons = 0; + } + dispatcher.cancel(e); + this.cleanupMouse(e.buttons); + } else { + dispatcher.move(e); + } + } + } + }, + mouseup: function(inEvent) { + if (!this.isEventSimulatedFromTouch(inEvent)) { + var e = this.prepareEvent(inEvent); + e.relatedTarget = scope.findTarget(inEvent); + e.target = pointermap.get(this.POINTER_ID); + dispatcher.up(e); + this.cleanupMouse(e.buttons); + } + }, + cleanupMouse: function(buttons) { + if (buttons === 0) { + pointermap.delete(this.POINTER_ID); + } + } + }; + + scope.mouseEvents = mouseEvents; +})(exports); + +(function(scope) { + var dispatcher = scope.dispatcher; + var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding); + var pointermap = dispatcher.pointermap; + var touchMap = Array.prototype.map.call.bind(Array.prototype.map); + // This should be long enough to ignore compat mouse events made by touch + var DEDUP_TIMEOUT = 2500; + var DEDUP_DIST = 25; + var CLICK_COUNT_TIMEOUT = 200; + var HYSTERESIS = 20; + var ATTRIB = 'touch-action'; + // TODO(dfreedm): disable until http://crbug.com/399765 is resolved + // var HAS_TOUCH_ACTION = ATTRIB in document.head.style; + var HAS_TOUCH_ACTION = false; + + // handler block for native touch events + var touchEvents = { + IS_IOS: false, + events: [ + 'touchstart', + 'touchmove', + 'touchend', + 'touchcancel' + ], + exposes: [ + 'down', + 'up', + 'move' + ], + register: function(target, initial) { + if (this.IS_IOS ? initial : !initial) { + dispatcher.listen(target, this.events); + } + }, + unregister: function(target) { + if (!this.IS_IOS) { + dispatcher.unlisten(target, this.events); + } + }, + scrollTypes: { + EMITTER: 'none', + XSCROLLER: 'pan-x', + YSCROLLER: 'pan-y', + }, + touchActionToScrollType: function(touchAction) { + var t = touchAction; + var st = this.scrollTypes; + if (t === st.EMITTER) { + return 'none'; + } else if (t === st.XSCROLLER) { + return 'X'; + } else if (t === st.YSCROLLER) { + return 'Y'; + } else { + return 'XY'; + } + }, + POINTER_TYPE: 'touch', + firstTouch: null, + isPrimaryTouch: function(inTouch) { + return this.firstTouch === inTouch.identifier; + }, + setPrimaryTouch: function(inTouch) { + // set primary touch if there no pointers, or the only pointer is the mouse + if (pointermap.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) { + this.firstTouch = inTouch.identifier; + this.firstXY = { + X: inTouch.clientX, + Y: inTouch.clientY + }; + this.firstTarget = inTouch.target; + this.scrolling = null; + this.cancelResetClickCount(); + } + }, + removePrimaryPointer: function(inPointer) { + if (inPointer.isPrimary) { + this.firstTouch = null; + this.firstXY = null; + this.resetClickCount(); + } + }, + clickCount: 0, + resetId: null, + resetClickCount: function() { + var fn = function() { + this.clickCount = 0; + this.resetId = null; + }.bind(this); + this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT); + }, + cancelResetClickCount: function() { + if (this.resetId) { + clearTimeout(this.resetId); + } + }, + typeToButtons: function(type) { + var ret = 0; + if (type === 'touchstart' || type === 'touchmove') { + ret = 1; + } + return ret; + }, + findTarget: function(touch, id) { + if (this.currentTouchEvent.type === 'touchstart') { + if (this.isPrimaryTouch(touch)) { + var fastPath = { + clientX: touch.clientX, + clientY: touch.clientY, + path: this.currentTouchEvent.path, + target: this.currentTouchEvent.target + }; + return scope.findTarget(fastPath); + } else { + return scope.findTarget(touch); + } + } + // reuse target we found in touchstart + return pointermap.get(id); + }, + touchToPointer: function(inTouch) { + var cte = this.currentTouchEvent; + var e = dispatcher.cloneEvent(inTouch); + // Spec specifies that pointerId 1 is reserved for Mouse. + // Touch identifiers can start at 0. + // Add 2 to the touch identifier for compatibility. + var id = e.pointerId = inTouch.identifier + 2; + e.target = this.findTarget(inTouch, id); + e.bubbles = true; + e.cancelable = true; + e.detail = this.clickCount; + e.buttons = this.typeToButtons(cte.type); + e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0; + e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0; + e.pressure = inTouch.webkitForce || inTouch.force || 0.5; + e.isPrimary = this.isPrimaryTouch(inTouch); + e.pointerType = this.POINTER_TYPE; + e._source = 'touch'; + // forward touch preventDefaults + var self = this; + e.preventDefault = function() { + self.scrolling = false; + self.firstXY = null; + cte.preventDefault(); + }; + return e; + }, + processTouches: function(inEvent, inFunction) { + var tl = inEvent.changedTouches; + this.currentTouchEvent = inEvent; + for (var i = 0, t, p; i < tl.length; i++) { + t = tl[i]; + p = this.touchToPointer(t); + if (inEvent.type === 'touchstart') { + pointermap.set(p.pointerId, p.target); + } + if (pointermap.has(p.pointerId)) { + inFunction.call(this, p); + } + if (inEvent.type === 'touchend' || inEvent._cancel) { + this.cleanUpPointer(p); + } + } + }, + // For single axis scrollers, determines whether the element should emit + // pointer events or behave as a scroller + shouldScroll: function(inEvent) { + if (this.firstXY) { + var ret; + var touchAction = scope.targetFinding.findTouchAction(inEvent); + var scrollAxis = this.touchActionToScrollType(touchAction); + if (scrollAxis === 'none') { + // this element is a touch-action: none, should never scroll + ret = false; + } else if (scrollAxis === 'XY') { + // this element should always scroll + ret = true; + } else { + var t = inEvent.changedTouches[0]; + // check the intended scroll axis, and other axis + var a = scrollAxis; + var oa = scrollAxis === 'Y' ? 'X' : 'Y'; + var da = Math.abs(t['client' + a] - this.firstXY[a]); + var doa = Math.abs(t['client' + oa] - this.firstXY[oa]); + // if delta in the scroll axis > delta other axis, scroll instead of + // making events + ret = da >= doa; + } + return ret; + } + }, + findTouch: function(inTL, inId) { + for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) { + if (t.identifier === inId) { + return true; + } + } + }, + // In some instances, a touchstart can happen without a touchend. This + // leaves the pointermap in a broken state. + // Therefore, on every touchstart, we remove the touches that did not fire a + // touchend event. + // To keep state globally consistent, we fire a + // pointercancel for this "abandoned" touch + vacuumTouches: function(inEvent) { + var tl = inEvent.touches; + // pointermap.pointers() should be < tl.length here, as the touchstart has not + // been processed yet. + if (pointermap.pointers() >= tl.length) { + var d = []; + pointermap.forEach(function(value, key) { + // Never remove pointerId == 1, which is mouse. + // Touch identifiers are 2 smaller than their pointerId, which is the + // index in pointermap. + if (key !== 1 && !this.findTouch(tl, key - 2)) { + var p = value; + d.push(p); + } + }, this); + d.forEach(function(p) { + this.cancel(p); + pointermap.delete(p.pointerId); + }, this); + } + }, + touchstart: function(inEvent) { + this.vacuumTouches(inEvent); + this.setPrimaryTouch(inEvent.changedTouches[0]); + this.dedupSynthMouse(inEvent); + if (!this.scrolling) { + this.clickCount++; + this.processTouches(inEvent, this.down); + } + }, + down: function(inPointer) { + dispatcher.down(inPointer); + }, + touchmove: function(inEvent) { + if (HAS_TOUCH_ACTION) { + // touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36 + // https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ + if (inEvent.cancelable) { + this.processTouches(inEvent, this.move); + } + } else { + if (!this.scrolling) { + if (this.scrolling === null && this.shouldScroll(inEvent)) { + this.scrolling = true; + } else { + this.scrolling = false; + inEvent.preventDefault(); + this.processTouches(inEvent, this.move); + } + } else if (this.firstXY) { + var t = inEvent.changedTouches[0]; + var dx = t.clientX - this.firstXY.X; + var dy = t.clientY - this.firstXY.Y; + var dd = Math.sqrt(dx * dx + dy * dy); + if (dd >= HYSTERESIS) { + this.touchcancel(inEvent); + this.scrolling = true; + this.firstXY = null; + } + } + } + }, + move: function(inPointer) { + dispatcher.move(inPointer); + }, + touchend: function(inEvent) { + this.dedupSynthMouse(inEvent); + this.processTouches(inEvent, this.up); + }, + up: function(inPointer) { + inPointer.relatedTarget = scope.findTarget(inPointer); + dispatcher.up(inPointer); + }, + cancel: function(inPointer) { + dispatcher.cancel(inPointer); + }, + touchcancel: function(inEvent) { + inEvent._cancel = true; + this.processTouches(inEvent, this.cancel); + }, + cleanUpPointer: function(inPointer) { + pointermap['delete'](inPointer.pointerId); + this.removePrimaryPointer(inPointer); + }, + // prevent synth mouse events from creating pointer events + dedupSynthMouse: function(inEvent) { + var lts = scope.mouseEvents.lastTouches; + var t = inEvent.changedTouches[0]; + // only the primary finger will synth mouse events + if (this.isPrimaryTouch(t)) { + // remember x/y of last touch + var lt = { + x: t.clientX, + y: t.clientY + }; + lts.push(lt); + var fn = (function(lts, lt) { + var i = lts.indexOf(lt); + if (i > -1) { + lts.splice(i, 1); + } + }).bind(null, lts, lt); + setTimeout(fn, DEDUP_TIMEOUT); + } + } + }; + + // prevent "ghost clicks" that come from elements that were removed in a touch handler + var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation; + document.addEventListener('click', function(ev) { + var x = ev.clientX, + y = ev.clientY; + // check if a click is within DEDUP_DIST px radius of the touchstart + var closeTo = function(touch) { + var dx = Math.abs(x - touch.x), + dy = Math.abs(y - touch.y); + return (dx <= DEDUP_DIST && dy <= DEDUP_DIST); + }; + // if click coordinates are close to touch coordinates, assume the click came from a touch + var wasTouched = scope.mouseEvents.lastTouches.some(closeTo); + // if the click came from touch, and the touchstart target is not in the path of the click event, + // then the touchstart target was probably removed, and the click should be "busted" + var path = scope.targetFinding.path(ev); + if (wasTouched) { + for (var i = 0; i < path.length; i++) { + if (path[i] === touchEvents.firstTarget) { + return; + } + } + ev.preventDefault(); + STOP_PROP_FN.call(ev); + } + }, true); + + scope.touchEvents = touchEvents; +})(exports); + +(function(scope) { + var dispatcher = scope.dispatcher; + var pointermap = dispatcher.pointermap; + var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number'; + var msEvents = { + events: [ + 'MSPointerDown', + 'MSPointerMove', + 'MSPointerUp', + 'MSPointerCancel', + ], + register: function(target) { + dispatcher.listen(target, this.events); + }, + unregister: function(target) { + if (target.nodeType === Node.DOCUMENT_NODE) { + return; + } + dispatcher.unlisten(target, this.events); + }, + POINTER_TYPES: [ + '', + 'unavailable', + 'touch', + 'pen', + 'mouse' + ], + prepareEvent: function(inEvent) { + var e = inEvent; + e = dispatcher.cloneEvent(inEvent); + if (HAS_BITMAP_TYPE) { + e.pointerType = this.POINTER_TYPES[inEvent.pointerType]; + } + e._source = 'ms'; + return e; + }, + cleanup: function(id) { + pointermap['delete'](id); + }, + MSPointerDown: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.target = scope.findTarget(inEvent); + pointermap.set(inEvent.pointerId, e.target); + dispatcher.down(e); + }, + MSPointerMove: function(inEvent) { + var target = pointermap.get(inEvent.pointerId); + if (target) { + var e = this.prepareEvent(inEvent); + e.target = target; + dispatcher.move(e); + } + }, + MSPointerUp: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.relatedTarget = scope.findTarget(inEvent); + e.target = pointermap.get(e.pointerId); + dispatcher.up(e); + this.cleanup(inEvent.pointerId); + }, + MSPointerCancel: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.relatedTarget = scope.findTarget(inEvent); + e.target = pointermap.get(e.pointerId); + dispatcher.cancel(e); + this.cleanup(inEvent.pointerId); + } + }; + + scope.msEvents = msEvents; +})(exports); + +(function(scope) { + var dispatcher = scope.dispatcher; + var pointermap = dispatcher.pointermap; + var pointerEvents = { + events: [ + 'pointerdown', + 'pointermove', + 'pointerup', + 'pointercancel' + ], + prepareEvent: function(inEvent) { + var e = dispatcher.cloneEvent(inEvent); + e._source = 'pointer'; + return e; + }, + register: function(target) { + dispatcher.listen(target, this.events); + }, + unregister: function(target) { + if (target.nodeType === Node.DOCUMENT_NODE) { + return; + } + dispatcher.unlisten(target, this.events); + }, + cleanup: function(id) { + pointermap['delete'](id); + }, + pointerdown: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.target = scope.findTarget(inEvent); + pointermap.set(e.pointerId, e.target); + dispatcher.down(e); + }, + pointermove: function(inEvent) { + var target = pointermap.get(inEvent.pointerId); + if (target) { + var e = this.prepareEvent(inEvent); + e.target = target; + dispatcher.move(e); + } + }, + pointerup: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.relatedTarget = scope.findTarget(inEvent); + e.target = pointermap.get(e.pointerId); + dispatcher.up(e); + this.cleanup(inEvent.pointerId); + }, + pointercancel: function(inEvent) { + var e = this.prepareEvent(inEvent); + e.relatedTarget = scope.findTarget(inEvent); + e.target = pointermap.get(e.pointerId); + dispatcher.cancel(e); + this.cleanup(inEvent.pointerId); + } + }; + + scope.pointerEvents = pointerEvents; +})(exports); + +/** + * This module contains the handlers for native platform events. + * From here, the dispatcher is called to create unified pointer events. + * Included are touch events (v1), mouse events, and MSPointerEvents. + */ +(function(scope) { + + var dispatcher = scope.dispatcher; + var nav = window.navigator; + + if (window.PointerEvent) { + dispatcher.registerSource('pointer', scope.pointerEvents); + } else if (nav.msPointerEnabled) { + dispatcher.registerSource('ms', scope.msEvents); + } else { + dispatcher.registerSource('mouse', scope.mouseEvents); + if (window.ontouchstart !== undefined) { + dispatcher.registerSource('touch', scope.touchEvents); + } + } + + // Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506 + var ua = navigator.userAgent; + var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window; + + dispatcher.IS_IOS = IS_IOS; + scope.touchEvents.IS_IOS = IS_IOS; + + dispatcher.register(document, true); +})(exports); + +/** + * This event denotes the beginning of a series of tracking events. + * + * @module PointerGestures + * @submodule Events + * @class trackstart + */ +/** + * Pixels moved in the x direction since trackstart. + * @type Number + * @property dx + */ +/** + * Pixes moved in the y direction since trackstart. + * @type Number + * @property dy + */ +/** + * Pixels moved in the x direction since the last track. + * @type Number + * @property ddx + */ +/** + * Pixles moved in the y direction since the last track. + * @type Number + * @property ddy + */ +/** + * The clientX position of the track gesture. + * @type Number + * @property clientX + */ +/** + * The clientY position of the track gesture. + * @type Number + * @property clientY + */ +/** + * The pageX position of the track gesture. + * @type Number + * @property pageX + */ +/** + * The pageY position of the track gesture. + * @type Number + * @property pageY + */ +/** + * The screenX position of the track gesture. + * @type Number + * @property screenX + */ +/** + * The screenY position of the track gesture. + * @type Number + * @property screenY + */ +/** + * The last x axis direction of the pointer. + * @type Number + * @property xDirection + */ +/** + * The last y axis direction of the pointer. + * @type Number + * @property yDirection + */ +/** + * A shared object between all tracking events. + * @type Object + * @property trackInfo + */ +/** + * The element currently under the pointer. + * @type Element + * @property relatedTarget + */ +/** + * The type of pointer that make the track gesture. + * @type String + * @property pointerType + */ +/** + * + * This event fires for all pointer movement being tracked. + * + * @class track + * @extends trackstart + */ +/** + * This event fires when the pointer is no longer being tracked. + * + * @class trackend + * @extends trackstart + */ + +(function(scope) { + var dispatcher = scope.dispatcher; + var eventFactory = scope.eventFactory; + var pointermap = new scope.PointerMap(); + var track = { + events: [ + 'down', + 'move', + 'up', + ], + exposes: [ + 'trackstart', + 'track', + 'trackx', + 'tracky', + 'trackend' + ], + defaultActions: { + 'track': 'none', + 'trackx': 'pan-y', + 'tracky': 'pan-x' + }, + WIGGLE_THRESHOLD: 4, + clampDir: function(inDelta) { + return inDelta > 0 ? 1 : -1; + }, + calcPositionDelta: function(inA, inB) { + var x = 0, + y = 0; + if (inA && inB) { + x = inB.pageX - inA.pageX; + y = inB.pageY - inA.pageY; + } + return { + x: x, + y: y + }; + }, + fireTrack: function(inType, inEvent, inTrackingData) { + var t = inTrackingData; + var d = this.calcPositionDelta(t.downEvent, inEvent); + var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent); + if (dd.x) { + t.xDirection = this.clampDir(dd.x); + } else if (inType === 'trackx') { + return; + } + if (dd.y) { + t.yDirection = this.clampDir(dd.y); + } else if (inType === 'tracky') { + return; + } + var gestureProto = { + bubbles: true, + cancelable: true, + trackInfo: t.trackInfo, + relatedTarget: inEvent.relatedTarget, + pointerType: inEvent.pointerType, + pointerId: inEvent.pointerId, + _source: 'track' + }; + if (inType !== 'tracky') { + gestureProto.x = inEvent.x; + gestureProto.dx = d.x; + gestureProto.ddx = dd.x; + gestureProto.clientX = inEvent.clientX; + gestureProto.pageX = inEvent.pageX; + gestureProto.screenX = inEvent.screenX; + gestureProto.xDirection = t.xDirection; + } + if (inType !== 'trackx') { + gestureProto.dy = d.y; + gestureProto.ddy = dd.y; + gestureProto.y = inEvent.y; + gestureProto.clientY = inEvent.clientY; + gestureProto.pageY = inEvent.pageY; + gestureProto.screenY = inEvent.screenY; + gestureProto.yDirection = t.yDirection; + } + var e = eventFactory.makeGestureEvent(inType, gestureProto); + t.downTarget.dispatchEvent(e); + }, + down: function(inEvent) { + if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) { + var p = { + downEvent: inEvent, + downTarget: inEvent.target, + trackInfo: {}, + lastMoveEvent: null, + xDirection: 0, + yDirection: 0, + tracking: false + }; + pointermap.set(inEvent.pointerId, p); + } + }, + move: function(inEvent) { + var p = pointermap.get(inEvent.pointerId); + if (p) { + if (!p.tracking) { + var d = this.calcPositionDelta(p.downEvent, inEvent); + var move = d.x * d.x + d.y * d.y; + // start tracking only if finger moves more than WIGGLE_THRESHOLD + if (move > this.WIGGLE_THRESHOLD) { + p.tracking = true; + p.lastMoveEvent = p.downEvent; + this.fireTrack('trackstart', inEvent, p); + } + } + if (p.tracking) { + this.fireTrack('track', inEvent, p); + this.fireTrack('trackx', inEvent, p); + this.fireTrack('tracky', inEvent, p); + } + p.lastMoveEvent = inEvent; + } + }, + up: function(inEvent) { + var p = pointermap.get(inEvent.pointerId); + if (p) { + if (p.tracking) { + this.fireTrack('trackend', inEvent, p); + } + pointermap.delete(inEvent.pointerId); + } + } + }; + dispatcher.registerGesture('track', track); +})(exports); + +/** + * This event is fired when a pointer is held down for 200ms. + * + * @module PointerGestures + * @submodule Events + * @class hold + */ +/** + * Type of pointer that made the holding event. + * @type String + * @property pointerType + */ +/** + * Screen X axis position of the held pointer + * @type Number + * @property clientX + */ +/** + * Screen Y axis position of the held pointer + * @type Number + * @property clientY + */ +/** + * Type of pointer that made the holding event. + * @type String + * @property pointerType + */ +/** + * This event is fired every 200ms while a pointer is held down. + * + * @class holdpulse + * @extends hold + */ +/** + * Milliseconds pointer has been held down. + * @type Number + * @property holdTime + */ +/** + * This event is fired when a held pointer is released or moved. + * + * @class release + */ + +(function(scope) { + var dispatcher = scope.dispatcher; + var eventFactory = scope.eventFactory; + var hold = { + // wait at least HOLD_DELAY ms between hold and pulse events + HOLD_DELAY: 200, + // pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold + WIGGLE_THRESHOLD: 16, + events: [ + 'down', + 'move', + 'up', + ], + exposes: [ + 'hold', + 'holdpulse', + 'release' + ], + heldPointer: null, + holdJob: null, + pulse: function() { + var hold = Date.now() - this.heldPointer.timeStamp; + var type = this.held ? 'holdpulse' : 'hold'; + this.fireHold(type, hold); + this.held = true; + }, + cancel: function() { + clearInterval(this.holdJob); + if (this.held) { + this.fireHold('release'); + } + this.held = false; + this.heldPointer = null; + this.target = null; + this.holdJob = null; + }, + down: function(inEvent) { + if (inEvent.isPrimary && !this.heldPointer) { + this.heldPointer = inEvent; + this.target = inEvent.target; + this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY); + } + }, + up: function(inEvent) { + if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { + this.cancel(); + } + }, + move: function(inEvent) { + if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) { + var x = inEvent.clientX - this.heldPointer.clientX; + var y = inEvent.clientY - this.heldPointer.clientY; + if ((x * x + y * y) > this.WIGGLE_THRESHOLD) { + this.cancel(); + } + } + }, + fireHold: function(inType, inHoldTime) { + var p = { + bubbles: true, + cancelable: true, + pointerType: this.heldPointer.pointerType, + pointerId: this.heldPointer.pointerId, + x: this.heldPointer.clientX, + y: this.heldPointer.clientY, + _source: 'hold' + }; + if (inHoldTime) { + p.holdTime = inHoldTime; + } + var e = eventFactory.makeGestureEvent(inType, p); + this.target.dispatchEvent(e); + } + }; + dispatcher.registerGesture('hold', hold); +})(exports); + +/** + * This event is fired when a pointer quickly goes down and up, and is used to + * denote activation. + * + * Any gesture event can prevent the tap event from being created by calling + * `event.preventTap`. + * + * Any pointer event can prevent the tap by setting the `tapPrevented` property + * on itself. + * + * @module PointerGestures + * @submodule Events + * @class tap + */ +/** + * X axis position of the tap. + * @property x + * @type Number + */ +/** + * Y axis position of the tap. + * @property y + * @type Number + */ +/** + * Type of the pointer that made the tap. + * @property pointerType + * @type String + */ +(function(scope) { + var dispatcher = scope.dispatcher; + var eventFactory = scope.eventFactory; + var pointermap = new scope.PointerMap(); + var tap = { + events: [ + 'down', + 'up' + ], + exposes: [ + 'tap' + ], + down: function(inEvent) { + if (inEvent.isPrimary && !inEvent.tapPrevented) { + pointermap.set(inEvent.pointerId, { + target: inEvent.target, + buttons: inEvent.buttons, + x: inEvent.clientX, + y: inEvent.clientY + }); + } + }, + shouldTap: function(e, downState) { + var tap = true; + if (e.pointerType === 'mouse') { + // only allow left click to tap for mouse + tap = (e.buttons ^ 1) && (downState.buttons & 1); + } + return tap && !e.tapPrevented; + }, + up: function(inEvent) { + var start = pointermap.get(inEvent.pointerId); + if (start && this.shouldTap(inEvent, start)) { + // up.relatedTarget is target currently under finger + var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget); + if (t) { + var e = eventFactory.makeGestureEvent('tap', { + bubbles: true, + cancelable: true, + x: inEvent.clientX, + y: inEvent.clientY, + detail: inEvent.detail, + pointerType: inEvent.pointerType, + pointerId: inEvent.pointerId, + altKey: inEvent.altKey, + ctrlKey: inEvent.ctrlKey, + metaKey: inEvent.metaKey, + shiftKey: inEvent.shiftKey, + _source: 'tap' + }); + t.dispatchEvent(e); + } + } + pointermap.delete(inEvent.pointerId); + } + }; + // patch eventFactory to remove id from tap's pointermap for preventTap calls + eventFactory.preventTap = function(e) { + return function() { + e.tapPrevented = true; + pointermap.delete(e.pointerId); + }; + }; + dispatcher.registerGesture('tap', tap); +})(exports); + +/* + * Basic strategy: find the farthest apart points, use as diameter of circle + * react to size change and rotation of the chord + */ + +/** + * @module pointer-gestures + * @submodule Events + * @class pinch + */ +/** + * Scale of the pinch zoom gesture + * @property scale + * @type Number + */ +/** + * Center X position of pointers causing pinch + * @property centerX + * @type Number + */ +/** + * Center Y position of pointers causing pinch + * @property centerY + * @type Number + */ + +/** + * @module pointer-gestures + * @submodule Events + * @class rotate + */ +/** + * Angle (in degrees) of rotation. Measured from starting positions of pointers. + * @property angle + * @type Number + */ +/** + * Center X position of pointers causing rotation + * @property centerX + * @type Number + */ +/** + * Center Y position of pointers causing rotation + * @property centerY + * @type Number + */ +(function(scope) { + var dispatcher = scope.dispatcher; + var eventFactory = scope.eventFactory; + var pointermap = new scope.PointerMap(); + var RAD_TO_DEG = 180 / Math.PI; + var pinch = { + events: [ + 'down', + 'up', + 'move', + 'cancel' + ], + exposes: [ + 'pinchstart', + 'pinch', + 'pinchend', + 'rotate' + ], + defaultActions: { + 'pinch': 'none', + 'rotate': 'none' + }, + reference: {}, + down: function(inEvent) { + pointermap.set(inEvent.pointerId, inEvent); + if (pointermap.pointers() == 2) { + var points = this.calcChord(); + var angle = this.calcAngle(points); + this.reference = { + angle: angle, + diameter: points.diameter, + target: scope.targetFinding.LCA(points.a.target, points.b.target) + }; + + this.firePinch('pinchstart', points.diameter, points); + } + }, + up: function(inEvent) { + var p = pointermap.get(inEvent.pointerId); + var num = pointermap.pointers(); + if (p) { + if (num === 2) { + // fire 'pinchend' before deleting pointer + var points = this.calcChord(); + this.firePinch('pinchend', points.diameter, points); + } + pointermap.delete(inEvent.pointerId); + } + }, + move: function(inEvent) { + if (pointermap.has(inEvent.pointerId)) { + pointermap.set(inEvent.pointerId, inEvent); + if (pointermap.pointers() > 1) { + this.calcPinchRotate(); + } + } + }, + cancel: function(inEvent) { + this.up(inEvent); + }, + firePinch: function(type, diameter, points) { + var zoom = diameter / this.reference.diameter; + var e = eventFactory.makeGestureEvent(type, { + bubbles: true, + cancelable: true, + scale: zoom, + centerX: points.center.x, + centerY: points.center.y, + _source: 'pinch' + }); + this.reference.target.dispatchEvent(e); + }, + fireRotate: function(angle, points) { + var diff = Math.round((angle - this.reference.angle) % 360); + var e = eventFactory.makeGestureEvent('rotate', { + bubbles: true, + cancelable: true, + angle: diff, + centerX: points.center.x, + centerY: points.center.y, + _source: 'pinch' + }); + this.reference.target.dispatchEvent(e); + }, + calcPinchRotate: function() { + var points = this.calcChord(); + var diameter = points.diameter; + var angle = this.calcAngle(points); + if (diameter != this.reference.diameter) { + this.firePinch('pinch', diameter, points); + } + if (angle != this.reference.angle) { + this.fireRotate(angle, points); + } + }, + calcChord: function() { + var pointers = []; + pointermap.forEach(function(p) { + pointers.push(p); + }); + var dist = 0; + // start with at least two pointers + var points = { + a: pointers[0], + b: pointers[1] + }; + var x, y, d; + for (var i = 0; i < pointers.length; i++) { + var a = pointers[i]; + for (var j = i + 1; j < pointers.length; j++) { + var b = pointers[j]; + x = Math.abs(a.clientX - b.clientX); + y = Math.abs(a.clientY - b.clientY); + d = x + y; + if (d > dist) { + dist = d; + points = { + a: a, + b: b + }; + } + } + } + x = Math.abs(points.a.clientX + points.b.clientX) / 2; + y = Math.abs(points.a.clientY + points.b.clientY) / 2; + points.center = { + x: x, + y: y + }; + points.diameter = dist; + return points; + }, + calcAngle: function(points) { + var x = points.a.clientX - points.b.clientX; + var y = points.a.clientY - points.b.clientY; + return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360; + } + }; + dispatcher.registerGesture('pinch', pinch); +})(exports); +},{}],9:[function(require,module,exports){ +'use strict'; + +var Base = require('extend-me').Base; + +var DataNodeBase = Base.extend('DataNodeBase', { + + isNullObject: false, + + INDENT: ' ', // 3 spaces + + initialize: function(key) { + this.label = key; + this.data = ['']; // TODO: Why is this first element needed? + this.index = []; // TODO: formerly rowIndex + this.hasChildren = false; // TODO: Where/how is this used? + this.depth = 0; + this.height = 1; + this.expanded = false; + }, + + getValue: function(x) { + return this.data[x]; + }, + + prune: function(depth) { + this.depth = depth; + this.data[0] = this.computeDepthString(); + }, + + computeDepthString: function() { + return Array(this.depth + 1).join(this.INDENT) + ' ' + this.label; + }, + + computeHeight: function() { + return 1; + }, + + getIndex: function() { // TODO: formerly getAllRowIndexes + return this.index; + }, + + computeAggregates: function(aggregator) { + var index = this.getIndex(); + + if (index.length) { + var groupsOffset = Number(aggregator.hasGroups()); + + // redimension the data + var data = this.data; + data.length = groupsOffset + aggregator.aggregates.length; + + var sorter = aggregator.sorterInstance; + sorter.index = index; + + aggregator.aggregates.forEach(function(aggregate, i) { + data[groupsOffset + i] = aggregate(sorter); + }); + } + }, + + buildView: function(aggregator) { + aggregator.addView(this); + }, + + toggleExpansionState: function() { /* aggregator */ + //do nothing by default + } + +}); + +//DataNodeBase.prototype.applyAggregates = DataNodeBase.prototype.computeAggregates; + +module.exports = DataNodeBase; +},{"extend-me":3}],10:[function(require,module,exports){ +'use strict'; + +var Map = require('./util/Mappy'); +var DataNodeBase = require('./DataNodeBase'); + +var expandedMap = { + true: '\u25bc', // BLACK DOWN-POINTING TRIANGLE aka '▼' + false: '\u25b6' // BLACK RIGHT-POINTING TRIANGLE aka '▶' +}; + +var DataNodeGroup = DataNodeBase.extend('DataNodeGroup', { + + extendable: true, + + initialize: function(key) { + this.children = new Map(); + }, + + prune: function(depth) { + this.depth = depth; + this.children = this.children.values; // TODO: why? + this.children.forEach(function(child) { + child.prune(depth + 1); + }); + this.data[0] = this.computeDepthString(); + }, + + computeDepthString: function() { + return Array(this.depth + 1).join(this.INDENT) + + expandedMap[this.expanded] + ' ' + + this.label; + }, + + getIndex: function() { + if (this.index.length === 0) { + this.index = this.computeIndex(); + } + return this.index; + }, + + computeIndex: function() { // TODO: formerly computerAllRowIndexes + var result = []; + result.append = append; + this.children.forEach(function(child) { + result.append(child.getIndex()); + }); + return result; + }, + + toggleExpansionState: function(aggregator) { /* aggregator */ + this.expanded = !this.expanded; + this.data[0] = this.computeDepthString(); + if (this.expanded) { + this.computeAggregates(aggregator); + } + }, + + computeAggregates: function(aggregator) { + DataNodeBase.prototype.computeAggregates.call(this, aggregator); // call base class's version + if (this.expanded) { + this.children.forEach(function(child) { + child.computeAggregates(aggregator); + }); + } + }, + + buildView: function(aggregator) { + aggregator.view.push(this); + if (this.expanded) { + this.children.forEach(function(child) { + child.buildView(aggregator); + }); + } + }, + + computeHeight: function() { + var height = 1; + + if (this.expanded) { + this.children.forEach(function(child) { + height = height + child.computeHeight(); + }); + } + + return (this.height = height); + } + +}); + +/** + * @summary Array mixin to append another array to end of `this` one. + * @desc Appends in place, unlike `this.concat()` which creates a new array. + * Uses less memory than concat, important when `appendix` is huge. + * > CAUTION: Mutates `this` array! + * @param {Array} appendix + * @returns {Array} Reference to `this` (for convenience) + */ +function append(appendix) { + this.splice.bind(this, this.length, 0).apply(this, appendix); + return this; +} + +module.exports = DataNodeGroup; + +},{"./DataNodeBase":9,"./util/Mappy":21}],11:[function(require,module,exports){ +'use strict'; + +var DataNodeBase = require('./DataNodeBase'); + +var DataNodeLeaf = DataNodeBase.extend('DataNodeLeaf', { + + prune: function(depth) { + this.depth = depth; + this.data[0] = this.computeDepthString(); + }, + + getIndex: function() { + return this.index; + }, + + buildView: function(aggregator) { + aggregator.addView(this); + }, + + computeHeight: function() { + return 1; + } + +}); + +module.exports = DataNodeLeaf; +},{"./DataNodeBase":9}],12:[function(require,module,exports){ +'use strict'; + +var DataNodeGroup = require('./DataNodeGroup'); + +var DataNodeTree = DataNodeGroup.extend('DataNodeTree', { + + initialize: function(key) { + this.height = 0; + this.expanded = true; + }, + + prune: function() { + this.children = this.children.values; + this.children.forEach(function(child) { + child.prune(0); + }); + }, + + buildView: function(aggregator) { + this.children.forEach(function(child) { + child.buildView(aggregator); + }); + }, + + computeHeight: function() { + var height = 1; + + this.children.forEach(function(child) { + height = height + child.computeHeight(); + }); + + return (this.height = height); + } + +}); + +module.exports = DataNodeTree; +},{"./DataNodeGroup":10}],13:[function(require,module,exports){ +'use strict'; + +var headerify = require('./util/headerify'); + +function DataSource(data, fields) { + this.fields = fields || computeFieldNames(data[0]); + this.data = data; +} + +DataSource.prototype = { + constructor: DataSource.prototype.constructor, // preserve constructor + + isNullObject: false, + + getRow: function(y) { + return this.data[y]; + }, + + getValue: function(x, y) { + var row = this.getRow(y); + if (!row) { + return null; + } + return row[this.fields[x]]; + }, + + setValue: function(x, y, value) { + this.getRow(y)[this.fields[x]] = value; + }, + + getRowCount: function() { + return this.data.length; + }, + + getColumnCount: function() { + return this.getFields().length; + }, + + getFields: function() { + return this.fields; + }, + + getHeaders: function() { + return ( + this.headers = this.headers || this.getDefaultHeaders().map(function(each) { + return headerify(each); + }) + ); + }, + + getDefaultHeaders: function() { + return this.getFields(); + }, + + setFields: function(fields) { + this.fields = fields; + }, + + setHeaders: function(headers) { + if (!(headers instanceof Array)) { + error('setHeaders', 'param #1 `headers` not array'); + } + this.headers = headers; + }, + + getGrandTotals: function() { + //nothing here + return; + }, + + setData: function(arrayOfUniformObjects) { + this.data = arrayOfUniformObjects; + } +}; + +function error(methodName, message) { + throw new Error('DataSource.' + methodName + ': ' + message); +} + +function computeFieldNames(object) { + if (!object) { + return []; + } + var fields = [].concat(Object.getOwnPropertyNames(object).filter(function(e) { + return e.substr(0, 2) !== '__'; + })); + return fields; +} + +module.exports = DataSource; + +},{"./util/headerify":23}],14:[function(require,module,exports){ +'use strict'; + +var _ = require('object-iterators'); + +var DataSourceSorter = require('./DataSourceSorter'); +var DataNodeTree = require('./DataNodeTree'); +var DataNodeGroup = require('./DataNodeGroup'); +var DataNodeLeaf = require('./DataNodeLeaf'); +var headerify = require('./util/headerify'); + +//?[t,c,b,a] +// t is a dataSource, +// a is a dictionary of aggregates, columnName:function +// b is a dictionary of groupbys, columnName:sourceColumnName +// c is a list of constraints, + +function DataSourceAggregator(dataSource) { + this.dataSource = dataSource; + this.tree = new DataNodeTree('Totals'); + this.index = []; + this.aggregates = []; + this.headers = []; + this.groupBys = []; + this.view = []; + this.sorterInstance = {}; + this.presortGroups = true; + this.lastAggregate = {}; + this.setAggregates({}); +} + +DataSourceAggregator.prototype = { + constructor: DataSourceAggregator.prototype.constructor, // preserve constructor + + isNullObject: false, + + setAggregates: function(aggregations) { + this.lastAggregate = aggregations; + this.clearAggregations(); + this.headers.length = 0; + + if (this.hasGroups()) { + this.headers.push('Tree'); + } + + var self = this; + _(aggregations).each(function(aggregation, key) { + self.addAggregate(key, aggregation); + }); + }, + + addAggregate: function(label, func) { + this.headers.push(headerify(label)); + this.aggregates.push(func); + }, + + setGroupBys: function(columnIndexArray) { + var groupBys = this.groupBys; + groupBys.length = 0; + columnIndexArray.forEach(function(columnIndex) { + groupBys.push(columnIndex); + }); + this.setAggregates(this.lastAggregate); + }, + + addGroupBy: function(index) { + this.groupBys.push(index); + }, + + hasGroups: function() { + return !!this.groupBys.length; + }, + + hasAggregates: function() { + return !!this.aggregates.length; + }, + + apply: function() { + this.buildGroupTree(); + }, + + clearGroups: function() { + this.groupBys.length = 0; + }, + + clearAggregations: function() { + this.aggregates.length = 0; + this.headers.length = 0; + }, + + buildGroupTree: function() { + var groupBys = this.groupBys, + leafDepth = groupBys.length - 1, + source = this.dataSource, + rowCount = source.getRowCount(), + tree = this.tree = new DataNodeTree('Totals'); + + // first sort data + if (this.presortGroups) { + groupBys.reverse().forEach(function(groupBy) { + source = new DataSourceSorter(source); + source.sortOn(groupBy); + }); + } + + for (var r = 0; r < rowCount; r++) { + var path = tree; + + groupBys.forEach(function(g, c) { // eslint-disable-line no-loop-func + var key = source.getValue(g, r), + terminalNode = (c === leafDepth), + Constructor = terminalNode ? DataNodeLeaf : DataNodeGroup, + ifAbsentFunc = createNode.bind(this, Constructor); + path = path.children.getIfAbsent(key, ifAbsentFunc); + }); + + path.index.push(r); + } + + this.sorterInstance = new DataSourceSorter(source); + tree.prune(); + tree.computeAggregates(this); + this.buildView(); + }, + + addView: function(dataNode) { + this.view.push(dataNode); + }, + + buildView: function() { + this.view.length = 0; + this.tree.computeHeight(); + this.tree.buildView(this); + }, + + viewMakesSense: function() { + return this.hasAggregates(); + }, + + getValue: function(x, y) { + if (!this.viewMakesSense()) { + return this.dataSource.getValue(x, y); + } + var row = this.view[y]; + if (!row) { + return null; + } + return row.getValue(x); // TODO: what kind of object is row... ? should it be unfiltred? + }, + + setValue: function(x, y, value) { + if (!this.viewMakesSense()) { + return this.dataSource.setValue(x, y, value); + } + }, + + getColumnCount: function() { + if (!this.viewMakesSense()) { + return this.dataSource.getColumnCount(); + } + return this.getHeaders().length; + }, + + getRowCount: function() { + if (!this.viewMakesSense()) { + return this.dataSource.getRowCount(); + } + return this.view.length; //header column + }, + + click: function(y) { + var group = this.view[y]; + group.toggleExpansionState(this); + this.buildView(); + }, + + getHeaders: function() { + if (!this.viewMakesSense()) { + return this.dataSource.getHeaders(); + } + return this.headers; // TODO: Views override dataSource headers with their own headers? + }, + + setHeaders: function(headers) { + this.dataSource.setHeaders(headers); + }, + + getFields: function() { + return this.dataSource.getFields(); + }, + + setFields: function(fields) { + return this.dataSource.setFields(fields); + }, + + getGrandTotals: function() { + var view = this.tree; + return [view.data]; + }, + + getRow: function(y) { + if (!this.viewMakesSense()) { + return this.dataSource.getRow(y); + } + + var rollups = this.view[y]; + + return rollups ? rollups : this.tree; + }, + + setData: function(arrayOfUniformObjects) { + this.dataSource.setData(arrayOfUniformObjects); + this.apply(); + } +}; + +function createNode(DataNodeConstructor, key, map) { + var value = new DataNodeConstructor(key); + map.set(key, value); + return value; +} + +module.exports = DataSourceAggregator; +},{"./DataNodeGroup":10,"./DataNodeLeaf":11,"./DataNodeTree":12,"./DataSourceSorter":18,"./util/headerify":23,"object-iterators":28}],15:[function(require,module,exports){ +'use strict'; + +var DataSourceIndexed = require('./DataSourceIndexed'); + +var DataSourceFilter = DataSourceIndexed.extend('DataSourceFilter', { + + initialize: function() { + this.filters = []; + }, + + add: function(columnIndex, filter) { + filter.columnIndex = columnIndex; + this.filters.push(filter); + }, + + clearAll: function() { + this.filters.length = 0; + this.clearIndex(); + }, + + applyAll: function() { + if (!this.filters.length) { + this.clearIndex(); + } else { + this.buildIndex(applyFilters); + } + }, + + getRowCount: function() { + return this.filters.length ? this.index.length : this.dataSource.getRowCount(); + }, + + aliases: { + set: 'add' + } +}); + +function applyFilters(r, rowObject) { // called in context from .buildIndex() + var self = this; + + // double negative here means "no filter fails" (i.e., row passes all filters) + return !this.filters.find(function(filter) { + return !filter(self.dataSource.getValue(filter.columnIndex, r), rowObject, r); + }); +} + +module.exports = DataSourceFilter; + +},{"./DataSourceIndexed":17}],16:[function(require,module,exports){ +'use strict'; + +var DataSourceIndexed = require('./DataSourceIndexed'); + +var DataSourceGlobalFilter = DataSourceIndexed.extend('DataSourceGlobalFilter', { + + set: function(filter) { + this.filter = filter; + }, + + clear: function() { + delete this.filter; + this.clearIndex(); + }, + + apply: function(visibleColumns) { + if (!this.filter) { + this.clearIndex(); + } else { + var visibleColumnMap = this.visibleColumnMap = []; + visibleColumns.forEach(function(column) { + visibleColumnMap.push(column.index); + }); + this.buildIndex(applyFilter); + } + }, + + getRowCount: function() { + return this.filter ? this.index.length : this.dataSource.getRowCount(); + } +}); + +function applyFilter(r, rowObject) { // called in context from .buildIndex() + var self = this; + return this.visibleColumnMap.find(function(columnIndex, mapIndex) { + var cellValue = self.dataSource.getValue(columnIndex, r); + return self.filter(cellValue, rowObject, r); + }); +} + +module.exports = DataSourceGlobalFilter; + +},{"./DataSourceIndexed":17}],17:[function(require,module,exports){ +'use strict'; + +var Base = require('extend-me').Base; + +var DataSourceIndexed = Base.extend('DataSourceIndexed', { + + isNullObject: false, + + initialize: function(dataSource) { + this.dataSource = dataSource; + this.index = []; + }, + + transposeY: function(y) { + return this.index.length ? this.index[y] : y; + }, + + getRow: function(y) { + return this.dataSource.getRow(this.transposeY(y)); + }, + + getValue: function(x, y) { + return this.dataSource.getValue(x, this.transposeY(y)); + }, + + setValue: function(x, y, value) { + this.dataSource.setValue(x, this.transposeY(y), value); + }, + + getRowCount: function() { + return this.index.length || this.dataSource.getRowCount(); + }, + + getColumnCount: function() { + return this.dataSource.getColumnCount(); + }, + + getFields: function() { + return this.dataSource.getFields(); + }, + + setFields: function(fields) { + return this.dataSource.setFields(fields); + }, + + setHeaders: function(headers) { + return this.dataSource.setHeaders(headers); + }, + + getHeaders: function() { + return this.dataSource.getHeaders(); + }, + + getGrandTotals: function() { + return this.dataSource.getGrandTotals(); + }, + + setData: function(arrayOfUniformObjects) { + return this.dataSource.setData(arrayOfUniformObjects); + }, + + clearIndex: function() { + this.index.length = 0; + }, + + buildIndex: function(predicate) { + var rowCount = this.dataSource.getRowCount(), + index = this.index; + + this.clearIndex(); + + for (var r = 0; r < rowCount; r++) { + if (!predicate || predicate.call(this, r, this.dataSource.getRow(r))) { + index.push(r); + } + } + + return index; + } + +}); + +module.exports = DataSourceIndexed; + +},{"extend-me":3}],18:[function(require,module,exports){ +'use strict'; + +var DataSourceIndexed = require('./DataSourceIndexed'); +var stableSort = require('./util/stableSort'); + +var DataSourceSorter = DataSourceIndexed.extend('DataSourceSorter', { + initialize: function() { + this.descendingSort = false; // TODO: this does not seem to be in use + }, + + sortOn: function(colIdx, direction) { + switch (direction) { + case 0: + this.clearIndex(); + break; + + case undefined: + case 1: + case -1: + var self = this; // for use in getValue + stableSort.sort(this.buildIndex(), getValue, direction); + break; + } + + function getValue(rowIdx) { + return valOrFuncCall(self.dataSource.getValue(colIdx, rowIdx)); + } + } +}); + +function valOrFuncCall(valOrFunc) { + return typeof valOrFunc === 'function' ? valOrFunc() : valOrFunc; +} + +module.exports = DataSourceSorter; +},{"./DataSourceIndexed":17,"./util/stableSort":24}],19:[function(require,module,exports){ +'use strict'; + +var DataSourceIndexed = require('./DataSourceIndexed'); +var DataSourceSorter = require('./DataSourceSorter'); + +var DataSourceSorterComposite = DataSourceIndexed.extend('DataSourceSorterComposite', { + initialize: function() { + this.sorts = []; + this.last = this.dataSource; + }, + + // Caveats regarding this.sorts: + // 1. Columns should be uniquely represented (i.e., no repeats with same columnIndex) + // 2. Columns should be added low- to high-order (i.e., most grouped columns come last) + sortOn: function(columnIndex, direction) { + this.sorts.push([columnIndex, direction]); + }, + + applySorts: function() { + var each = this.dataSource; + + this.sorts.forEach(function(sort) { + each = new DataSourceSorter(each); + each.sortOn.apply(each, sort); + }); + + this.last = each; + }, + + clearSorts: function() { + this.sorts.length = 0; + this.last = this.dataSource; + }, + + getValue: function(x, y) { + return this.last.getValue(x, y); + }, + + setValue: function(x, y, value) { + this.last.setValue(x, y, value); + } +}); + +module.exports = DataSourceSorterComposite; +},{"./DataSourceIndexed":17,"./DataSourceSorter":18}],20:[function(require,module,exports){ +'use strict'; + +module.exports = { + JSDataSource: require('./DataSource'), + DataSourceSorter: require('./DataSourceSorter'), + DataSourceSorterComposite: require('./DataSourceSorterComposite'), + DataSourceFilter: require('./DataSourceFilter'), + DataSourceGlobalFilter: require('./DataSourceGlobalFilter'), + DataSourceAggregator: require('./DataSourceAggregator'), + util: { + aggregations: require('./util/aggregations') + } +}; +},{"./DataSource":13,"./DataSourceAggregator":14,"./DataSourceFilter":15,"./DataSourceGlobalFilter":16,"./DataSourceSorter":18,"./DataSourceSorterComposite":19,"./util/aggregations":22}],21:[function(require,module,exports){ +'use strict'; + +function Mappy() { + this.keys = []; + this.data = {}; + this.values = []; +} + +Mappy.prototype = { + + constructor: Mappy.prototype.constructor, // preserve constructor + + set: function(key, value) { + var hashCode = hash(key); + if (!(hashCode in this.data)) { + this.keys.push(key); + this.values.push(value); + } + this.data[hashCode] = value; + }, + + get: function(key) { + var hashCode = hash(key); + return this.data[hashCode]; + }, + + getIfAbsent: function(key, ifAbsentFunc) { + var value = this.get(key); + if (value === undefined) { + value = ifAbsentFunc(key, this); + } + return value; + }, + + size: function() { + return this.keys.length; + }, + + clear: function() { + this.keys.length = 0; + // TODO: Is there a reason why this.values is not being truncated here as well? + this.data = {}; + }, + + delete: function(key) { + var hashCode = hash(key); + if (this.data[hashCode] !== undefined) { + var index = betterIndexOf(this.keys, key); + this.keys.splice(index, 1); + this.values.splice(index, 1); + delete this.data[hashCode]; + } + }, + + forEach: function(func) { + var keys = this.keys, + self = this; + keys.forEach(function(key) { + var value = self.get(key); + func(value, key, self); + }); + }, + + map: function(func) { + var keys = this.keys, + newMap = new Mappy(), + self = this; + keys.forEach(function(key) { + var value = self.get(key), + transformed = func(value, key, self); + newMap.set(key, transformed); + }); + return newMap; + }, + + copy: function() { + var keys = this.keys, + newMap = new Mappy(), + self = this; + keys.forEach(function(key) { + var value = self.get(key); + newMap.set(key, value); + }); + return newMap; + } + +}; + +var OID_PREFIX = '.~.#%_'; //this should be something we never will see at the beginning of a string +var counter = 0; + +function hash(key) { + var typeOf = typeof key; + + switch (typeOf) { + case 'number': + case 'string': + case 'boolean': + case 'symbol': + return OID_PREFIX + typeOf + '_' + key; + + case 'undefined': + return OID_PREFIX + 'undefined'; + + case 'object': + // TODO: what about handling null (special case of object)? + case 'function': + return (key.___finhash = key.___finhash || OID_PREFIX + counter++); // eslint-disable-line + } +} + +// Object.is polyfill, courtesy of @WebReflection +var is = Object.is || function(a, b) { + return a === b ? a !== 0 || 1 / a == 1 / b : a != a && b != b; // eslint-disable-line +}; + +// More reliable indexOf, courtesy of @WebReflection +function betterIndexOf(arr, value) { + if (value != value || value === 0) { // eslint-disable-line + for (var i = arr.length; i-- && !is(arr[i], value);) { // eslint-disable-line + } + } else { + i = [].indexOf.call(arr, value); + } + return i; +} + +module.exports = Mappy; +},{}],22:[function(require,module,exports){ +'use strict'; + +function count(group) { + return group.getRowCount(); +} + +function sum(columnIndex, group) { + var r = group.getRowCount(), + n = 0; + + while (r--) { + n += group.getValue(columnIndex, r); + } + + return n; +} + +function minmax(columnIndex, method, n, group) { + var r = group.getRowCount(); + + while (r--) { + n = method(n, group.getValue(columnIndex, r)); + } + + return n; +} + +function avg(columnIndex, group) { + return sum(columnIndex, group) / group.getRowCount(); +} + +function first(columnIndex, group) { + return group.getValue(columnIndex, 0); +} + +function last(columnIndex, group) { + return group.getValue(columnIndex, group.getRowCount() - 1); +} + +function stddev(columnIndex, group) { + var rows = group.getRowCount(), + mean = avg(columnIndex, group); + + for (var dev, r = rows, variance = 0; r--; variance += dev * dev) { + dev = group.getValue(columnIndex, r) - mean; + } + + return Math.sqrt(variance / rows); +} + +module.exports = { + count: function(columnIndex) { + return count; + }, + sum: function(columnIndex) { + return sum.bind(this, columnIndex); + }, + min: function(columnIndex) { + return minmax.bind(this, columnIndex, Math.min, Infinity); + }, + max: function(columnIndex) { + return minmax.bind(this, columnIndex, Math.max, -Infinity); + }, + avg: function(columnIndex) { + return avg.bind(this, columnIndex); + }, + first: function(columnIndex) { + return first.bind(this, columnIndex); + }, + last: function(columnIndex) { + return last.bind(this, columnIndex); + }, + stddev: function(columnIndex) { + return stddev.bind(this, columnIndex); + } +}; +},{}],23:[function(require,module,exports){ +'use strict'; + +function headerify(string) { + return (/[a-z]/.test(string) ? string : string.toLowerCase()) + .replace(/[\s\-_]*([^\s\-_])([^\s\-_]+)/g, replacer) + .replace(/[A-Z]/g, ' $&') + .trim(); +} + +function replacer(a, b, c) { + return b.toUpperCase() + c; +} + +module.exports = headerify; +},{}],24:[function(require,module,exports){ +'use strict'; + +function stabilize(comparator, descending, arr1, arr2) { // eslint-disable-line no-shadow + var x = arr1[0]; + var y = arr2[0]; + + if (x === y) { + x = descending ? arr2[1] : arr1[1]; + y = descending ? arr1[1] : arr2[1]; + } else { + if (y === null) { + return -1; + } + if (x === null) { + return 1; + } + } + + return comparator(x, y); +} + +function ascendingNumbers(x, y) { + return x - y; +} + +function descendingNumbers(x, y) { + return y - x; +} + +function ascendingAllOthers(x, y) { + return x < y ? -1 : 1; +} + +function descendingAllOthers(x, y) { + return y < x ? -1 : 1; +} + +function ascending(typeOfData) { + return stabilize.bind(this, typeOfData === 'number' ? ascendingNumbers : ascendingAllOthers, false); +} + +function descending(typeOfData) { + return stabilize.bind(this, typeOfData === 'number' ? descendingNumbers : descendingAllOthers, true); +} + +function sort(index, getValue, direction) { + + var compare, i; + + // apply defaults + if (direction === undefined) { + direction = 1; + } + + if (index.length) { // something to do + switch (direction) { + case 0: + return; // bail: nothing to sort + + case undefined: // eslint-disable-line no-fallthrough + direction = 1; + case 1: + compare = ascending(typeof getValue(0)); + break; + + case -1: + compare = descending(typeof getValue(0)); + break; + } + + // set up the sort..... + var tmp = new Array(index.length); + + // add the index for "stability" + for (i = 0; i < index.length; i++) { + tmp[i] = [getValue(i), i]; + } + + // do the actual sort + tmp.sort(compare); + + // copy the sorted values into our index vector + for (i = 0; i < index.length; i++) { + index[i] = tmp[i][1]; + } + } + +} + +exports.sort = sort; +},{}],25:[function(require,module,exports){ +// list-dragon node module +// https://github.com/openfin/list-dragon + +/* eslint-env node, browser */ + +'use strict'; + +var cssInjector = require('css-injector'); +var format = require('templex'); + +var REVERT_TO_STYLESHEET_VALUE = null; // null removes the style + +var transform, timer, scrollVelocity, cssListDragon; + +/* inject:css */ +cssListDragon = 'div.dragon-list{position:relative;background-color:#fff}div.dragon-list>div,div.dragon-list>ul{position:absolute;left:0;right:0}div.dragon-list>div{text-align:center;background-color:#00796b;color:#fff;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);overflow:hidden;white-space:nowrap}div.dragon-list>ul{overflow-y:auto;bottom:0;margin:0;padding:0;box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}div.dragon-list>ul>li,li.dragon-pop{white-space:nowrap;list-style-type:none;border:0 solid #f4f4f4;border-bottom:1px solid #e0e0e0;cursor:move;transition:border-top-width .2s}div.dragon-list>ul>li:last-child{height:0;border-bottom:none}li.dragon-pop{position:fixed;background-color:#fff;border:1px solid #e0e0e0;left:0;top:0;overflow-x:hidden;box-shadow:rgba(0,0,0,.188235) 0 10px 20px,rgba(0,0,0,.227451) 0 6px 6px}'; +/* endinject */ + +/** + * @constructor ListDragon + * + * @desc This object services a set of item lists that allow dragging and dropping items within and between lists in a set. + * + * Two strategies are supported: + * + * 1. Supply your own HTML markup and let the API build the item models for you. + * To use this strategy, script your HTML and provide one of these: + * * an array of all the list item (`
  • `) tags + * * a CSS selector that points to all the list item tags + * 2. Supply your own item models and let the API build the HTML markup for you. + * To use this strategy, provide an array of model lists. + * + * The new ListDragon object's `modelLists` property references the array of model lists the API constructed for you in strategy #1 or the array of model lists you supplied for strategy #2. + * + * After the user performs a successful drag-and-drop operation, the position of the model references within the `modelLists` array is rearranged. (The models themselves are the original objects as supplied in the model lists; they are not rebuilt or altered in any way. Just the references to them are moved around.) + * + * @param {string|Element[]|modelListType[]} selectorOrModelLists - You must supply one of the items in **bold** below: + * + * 1. _For strategy #1 above (API creates models from supplied elements):_ All the list item (`
  • `) DOM elements of all the lists you want the new object to manage, as either: + * 1. **A CSS selector;** _or_ + * 2. **An array of DOM elements** + * 2. _For strategy #2 above (API creates elements from supplied models):_ **An array of model lists,** each of which is in one of the following forms: + * 1. An array of item models (with various option properties hanging off of it); _and/or_ + * 2. A {@link modelListType} object with those same various option properties including the required `models` property containing that same array of item models. + * + * In either case (2.1 or 2.2), each element of such arrays of item models may take the form of: + * * A string primitive; _or_ + * * A {@link itemModelType} object with a various option properties including the required `label` property containing a string primitive. + * + * Regarding these string primitives, each is either: + * * A string to be displayed in the list item; _or_ + * * A format string with other property values merged in, the result of which is to be displayed in the list item. + * + * @param {object} [options={}] - You may supply "global" template variables here, representing the "outer scope," after first searching each model and then each model list. + * @param {undefined|null|Element|string} [cssStylesheetReferenceElement] - Determines where to insert the stylesheet. (This is the only formal option.) Passed to css-injector, the overloads are (from css-injector docs): + * * `undefined` type (or omitted): injects stylesheet at top of `...` element + * * `null` value: injects stylesheet at bottom of `...` element + * * `Element` type: injects stylesheet immediately before given element, wherever it is found. + * * `string` type: injects stylesheet immediately before given first element found that matches the given css selector. + */ +function ListDragon(selectorOrModelLists, options) { + + if (!(this instanceof ListDragon)) { + throw error('Not called with "new" keyword.'); + } + + var self = this, modelLists, items; + + options = options || {}; + + if (typeof selectorOrModelLists === 'string') { + items = toArray(document.querySelectorAll(selectorOrModelLists)); + modelLists = createModelListsFromListElements(items); + } else if (selectorOrModelLists[0] instanceof Element) { + items = toArray(selectorOrModelLists); + modelLists = createModelListsFromListElements(items); + } else { + // param is array of model lists + // build new
      element(s) for each list and put in `.modelLists`; + // fill `.items` array with
    • elements from these new
        elements + items = []; + modelLists = createListElementsFromModelLists(selectorOrModelLists, options); + modelLists.forEach(function (list) { + items = items.concat(toArray(list.element.querySelectorAll('li'))); + }); + } + + // grab wheel events and don't let 'em bubble + modelLists.forEach(function (modelList) { + modelList.element.addEventListener('wheel', captureEvent); + }); + + items.forEach(function (itemElement, index) { + var item = (itemElement !== itemElement.parentElement.lastElementChild) + ? self.addEvt(itemElement, 'mousedown', itemElement, true) + : { element: itemElement }; + + /* `item.model` not currently needed so commented out here. + * (Originally used for rebuilding modelLists for final + * reporting, modelLists are now spliced on every successful + * drag-and-drop operation so they're always up to date.) + + var origin = this.itemCoordinates(itemElement); + item.model = this.modelLists[origin.list].models[origin.item]; + + */ + + items[index] = item; + }); + + transform = 'transform' in items[0].element.style + ? 'transform' // Chrome 45 and Firefox 40 + : '-webkit-transform'; // Safari 8 + + // set up the new object + this.modelLists = modelLists; + this.items = items; + this.bindings = {}; + this.callback = {}; + + cssInjector(cssListDragon, 'list-dragon-base', options.cssStylesheetReferenceElement); + +} + +ListDragon.prototype = { + + addEvt: function (target, type, listener, doNotBind) { + var binding = { + handler: handlers[type].bind(target, this), + element: listener || window + }; + + if (!doNotBind) { + this.bindings[type] = binding; + } + + binding.element.addEventListener(type, binding.handler); + + return binding; + }, + + removeEvt: function (type) { + var binding = this.bindings[type]; + delete this.bindings[type]; + binding.element.removeEventListener(type, binding.handler); + }, + + removeAllEventListeners: function () { + // remove drag & drop events (mousemove, mouseup, and transitionend) + for (var type in this.bindings) { + var binding = this.bindings[type]; + binding.element.removeEventListener(type, binding.handler); + } + // remove the mousedown events from all list items + this.items.forEach(function (item) { + if (item.handler) { + item.element.removeEventListener('mousedown', item.handler); + } + }); + // wheel events on the list elements + this.modelLists.forEach(function (modelList) { + modelList.element.removeEventListener('wheel', captureEvent); + }); + }, + + pointInListRects: function (point) { + return this.modelLists.find(function (modelList) { + var rect = modelList.element.getBoundingClientRect(); + + rect = { + left: window.scrollX + rect.left, + top: window.scrollY + rect.top, + right: window.scrollX + rect.right, + bottom: window.scrollY + rect.bottom, + width: rect.width, + height: rect.height + }; + + modelList.rect = rect; + + if (pointInRect(point, rect)) { + modelList.rect = rect; + return true; // found + } else { + return false; + } + }); + }, + + pointInItemRects: function (point, except1, except2) { + return this.items.find(function (item) { + var element = item.element; + return ( + element !== except1 && + element !== except2 && + pointInRect(point, item.rect) + ); + }); + }, + + // get positions of all list items in page coords (normalized for window and list scrolling) + getAllItemBoundingRects: function () { + var modelLists = this.modelLists, height; + this.items.forEach(function (item) { + var itemElement = item.element, + listElement = itemElement.parentElement, + list = modelLists.find(function (list) { return list.element === listElement; }); + + if ( + // omitted: default to true + list.isDropTarget === undefined || + + // function: use return value + typeof list.isDropTarget === 'function' && list.isDropTarget() || + + // otherwise: use truthiness of given value + list.isDropTarget + ) { + var rect = itemElement.getBoundingClientRect(), + bottom = rect.bottom; + + if (itemElement === listElement.lastElementChild) { + bottom = listElement.getBoundingClientRect().bottom; + if (bottom < rect.top) { + bottom = rect.top + (height || 50); + } + } else { + height = rect.height; + } + + rect = { + left: window.scrollX + rect.left, + right: window.scrollX + rect.right, + top: window.scrollY + rect.top + listElement.scrollTop, + bottom: window.scrollY + bottom + listElement.scrollTop + }; + + item.rect = rect; + } + }); + }, + + reinsert: function (target) { + var style = target.style; + style.width = style[transform] = style.transition = REVERT_TO_STYLESHEET_VALUE; + + target.classList.remove('dragon-pop'); + + this.drop.style.transitionDuration = '0s'; + this.drop.style.borderTopWidth = REVERT_TO_STYLESHEET_VALUE; + this.drop.parentElement.insertBefore(target, this.drop); + + delete this.drop; + }, + + // return an object { item: , list: } + itemCoordinates: function (item) { + var listElement = item.parentElement, + coords = { item: 0 }; + + while ((item = item.previousElementSibling)) { + ++coords.item; + } + + this.modelLists.find(function (list, index) { + coords.list = index; + return list.element === listElement; // stop when we find the one we belong to + }); + + return coords; + } + +}; + +var handlers = { + mousedown: function (dragon, evt) { + + evt.stopPropagation(); + evt.preventDefault(); //prevents user selection of rendered nodes during drag + + if (dragon.drop) { + return; + } + + var rect = this.getBoundingClientRect(); + + dragon.rect = rect = { + left: Math.round(rect.left - 1), + top: Math.round(rect.top - 1), + right: Math.round(rect.right), + bottom: Math.round(rect.bottom), + width: Math.round(rect.width), + height: Math.round(rect.height) + }; + + dragon.pin = { + x: window.scrollX + evt.clientX, + y: window.scrollY + evt.clientY + }; + + dragon.origin = dragon.itemCoordinates(this); + + if (dragon.callback.grabbed) { + dragon.callback.grabbed.call(this, dragon); + } + + dragon.getAllItemBoundingRects(); + + dragon.drop = this.nextElementSibling; + dragon.drop.style.transitionDuration = '0s'; + dragon.drop.style.borderTopWidth = rect.height + 'px'; + + this.style.width = rect.width + 'px'; + this.style.transitionDuration = '0s'; + this.style[transform] = translate( + rect.left - window.scrollX, + rect.top - window.scrollY + ); + this.classList.add('dragon-pop'); + this.style.zIndex = window.getComputedStyle(dragon.modelLists[0].container.parentElement).zIndex; + + if (!dragon.container) { + // walk back to closest shadow root OR body tag OR root tag + var container = this; + while (container.parentNode) { + container = container.parentNode; + if (container instanceof ShadowRoot || container.tagName === 'BODY'){ + break; + } + } + dragon.container = container; + } + + dragon.container.appendChild(this); + + rect.left += window.scrollX; + rect.top += window.scrollY; + rect.right += window.scrollX; + rect.bottom += window.scrollY; + + dragon.addEvt(this, 'mousemove'); + dragon.addEvt(this, 'mouseup'); + }, + + mousemove: function (dragon, evt) { + dragon.drop.style.transition = REVERT_TO_STYLESHEET_VALUE; + + var hoverList = dragon.pointInListRects({ x: evt.clientX, y: evt.clientY }) || dragon.mostRecentHoverList; + + if (hoverList) { + var dx = evt.clientX - dragon.pin.x, + dy = evt.clientY - dragon.pin.y; + + dragon.mostRecentHoverList = hoverList; + + var maxScrollY = hoverList.element.scrollHeight - hoverList.rect.height, + y = evt.clientY + window.scrollY, + magnitude; + + if (maxScrollY > 0) { + // list is scrollable (is taller than rect) + if (hoverList.element.scrollTop > 0 && (magnitude = y - (hoverList.rect.top + 5)) < 0) { + // mouse near or above top and list is not scrolled to top yet + resetAutoScrollTimer(magnitude, 0, hoverList.element); + } else if (hoverList.element.scrollTop < maxScrollY && (magnitude = y - (hoverList.rect.bottom - 1 - 5)) > 0) { + // mouse near or below bottom and list not scrolled to bottom yet + resetAutoScrollTimer(magnitude, maxScrollY, hoverList.element); + } else { + // mouse inside + resetAutoScrollTimer(); + } + } + + var other = dragon.pointInItemRects({ + x: evt.clientX, + y: dragon.rect.bottom + window.scrollY + dy + hoverList.element.scrollTop + }, this, dragon.drop); + + this.style[transform] = translate( + dragon.rect.left - window.scrollX + dx, + dragon.rect.top - window.scrollY + dy + ); + + if (other) { + var element = other.element; + element.style.transition = REVERT_TO_STYLESHEET_VALUE; + element.style.borderTopWidth = dragon.drop.style.borderTopWidth; + dragon.drop.style.borderTopWidth = null; + dragon.drop = element; + } + } + }, + + mouseup: function (dragon, evt) { + resetAutoScrollTimer(); + dragon.removeEvt('mousemove'); + dragon.removeEvt('mouseup'); + + evt.stopPropagation(); + + var newRect = this.getBoundingClientRect(); + + if ( + window.scrollX + newRect.left === dragon.rect.left && + window.scrollY + newRect.top === dragon.rect.top + ) { + dragon.reinsert(this); + } else { + var dropRect = dragon.drop.getBoundingClientRect(); + + dragon.addEvt(this, 'transitionend', this); + this.style.transitionDuration = REVERT_TO_STYLESHEET_VALUE; //reverts to 200ms + this.style.transitionProperty = transform; + this.style[transform] = translate( + dropRect.left - window.scrollX, + dropRect.top - window.scrollY + ); + } + }, + + transitionend: function (dragon, evt) { + if (evt.propertyName === transform) { + dragon.removeEvt('transitionend'); + dragon.reinsert(this); + + this.style.transitionProperty = REVERT_TO_STYLESHEET_VALUE; //reverts to border-top-width + + var model = dragon.modelLists[dragon.origin.list].splice(dragon.origin.item, 1)[0]; + var destination = dragon.itemCoordinates(this); + dragon.modelLists[destination.list].splice(destination.item, 0, model); + + if (dragon.callback.dropped) { + dragon.callback.dropped.call(this, dragon); + } + } + } +}; + +function resetAutoScrollTimer(magnitude, limit, element) { + if (!magnitude) { + clearInterval(timer); + scrollVelocity = 0; + } else { + var changeDirection = + scrollVelocity < 0 && magnitude >= 0 || + scrollVelocity === 0 && magnitude !== 0 || + scrollVelocity > 0 && magnitude <= 0; + scrollVelocity = magnitude > 0 ? Math.min(50, magnitude) : Math.max(-50, magnitude); + if (changeDirection) { + clearInterval(timer); + timer = setInterval(function (limit) { + var scrollTop = element.scrollTop + scrollVelocity; + if (scrollVelocity < 0 && scrollTop < limit || scrollVelocity > 0 && scrollTop > limit) { + element.scrollTop = limit; + clearInterval(timer); + } else { + element.scrollTop = scrollTop; + } + }, 125); + } + } +} + +function toArray(arrayLikeObject) { + return Array.prototype.slice.call(arrayLikeObject); +} + +function pointInRect(point, rect) { + return rect.top <= point.y && point.y <= rect.bottom + && rect.left <= point.x && point.x <= rect.right; +} + +function translate(left, top) { + return 'translate(' + + Math.floor(left + window.scrollX) + 'px,' + + Math.floor(top + window.scrollY) + 'px)'; +} + +function htmlEncode(string) { + var textNode = document.createTextNode(string); + + return document + .createElement('a') + .appendChild(textNode) + .parentNode + .innerHTML; +} + +/** + * Creates `
          ...
        ` elements and inserts them into an `element` property on each model. + * @param {object} modelLists + * @returns `modelLists` + */ +function createListElementsFromModelLists(modelLists, options) { + var templateLabel = options.label || '{label}'; + + modelLists.forEach(function (modelList, listIndex) { + var listLabel = modelList.label || templateLabel, + listHtmlEncode = modelList.htmlEncode !== undefined && modelList.htmlEncode || options.htmlEncode, + container = document.createElement('div'), + listElement = document.createElement('ul'); + + if (modelList.models) { + Object.keys(modelList).forEach(function (key) { + if (key !== 'models') { + modelList.models[key] = modelList[key]; + } + }); + modelLists[listIndex] = modelList = modelList.models; + } else if (modelList instanceof Array) { + modelList.models = modelList; // point to self + } else { + throw error('List [{1}] not an array of models (with or without additional properties) OR ' + + 'an object (with a `models` property containing an array of models).', listIndex); + } + + modelList.forEach(function (model) { + var modelLabel = model.label || listLabel, + modelHtmlEncode = model.htmlEncode !== undefined && model.htmlEncode || listHtmlEncode, + modelObject = typeof model === 'object' ? model : { label: model}, + label = format.call([modelObject, modelList, options], modelLabel), + itemElement = document.createElement('li'); + + itemElement.innerHTML = modelHtmlEncode ? htmlEncode(label) : label; + + listElement.appendChild(itemElement); + }); + + // append the final "fencepost" item -- drop target at bottom of list after all items + var itemElement = document.createElement('li'); + itemElement.innerHTML = ' '; + listElement.appendChild(itemElement); + + // append header to container + if (modelList.title) { + var header = document.createElement('div'); + header.innerHTML = listHtmlEncode ? htmlEncode(modelList.title) : modelList.title; + container.appendChild(header); + } + + container.appendChild(listElement); + container.className = modelList.cssClassNames || options.cssClassNames || 'dragon-list'; + modelList.element = listElement; + modelList.container = container; + }); + + return modelLists; +} + +/** + * Create a `.modelLists` array with these
      • elements' parent
          elements + * @param {Element[]} listItemElements + * @returns {Array} + */ +function createModelListsFromListElements(listItemElements) { + var modelLists = []; + + listItemElements.forEach(function (itemElement) { + var listElement = itemElement.parentElement, + container = listElement.parentElement, + models = []; + if (!modelLists.find(function (list) { return list.element === listElement; })) { + toArray(listElement.querySelectorAll('li')).forEach(function (itemElement) { + if (itemElement !== listElement.lastElementChild) { + models.push(itemElement.innerHTML); + } + }); + models.element = listElement; + models.container = container; + modelLists.push(models); + } + }); + + return modelLists; +} + +function captureEvent(evt) { + evt.stopPropagation(); +} + +function error() { + return 'list-dragon: ' + format.apply(this, Array.prototype.slice.call(arguments)); +} + +// this interface consists solely of the prototypal object constructor +module.exports = ListDragon; + +},{"css-injector":2,"templex":31}],26:[function(require,module,exports){ +;(function () { // closure for web browsers + +if (typeof module === 'object' && module.exports) { + module.exports = LRUCache +} else { + // just set the global for non-node platforms. + this.LRUCache = LRUCache +} + +function hOP (obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key) +} + +function naiveLength () { return 1 } + +var didTypeWarning = false +function typeCheckKey(key) { + if (!didTypeWarning && typeof key !== 'string' && typeof key !== 'number') { + didTypeWarning = true + console.error(new TypeError("LRU: key must be a string or number. Almost certainly a bug! " + typeof key).stack) + } +} + +function LRUCache (options) { + if (!(this instanceof LRUCache)) + return new LRUCache(options) + + if (typeof options === 'number') + options = { max: options } + + if (!options) + options = {} + + this._max = options.max + // Kind of weird to have a default max of Infinity, but oh well. + if (!this._max || !(typeof this._max === "number") || this._max <= 0 ) + this._max = Infinity + + this._lengthCalculator = options.length || naiveLength + if (typeof this._lengthCalculator !== "function") + this._lengthCalculator = naiveLength + + this._allowStale = options.stale || false + this._maxAge = options.maxAge || null + this._dispose = options.dispose + this.reset() +} + +// resize the cache when the max changes. +Object.defineProperty(LRUCache.prototype, "max", + { set : function (mL) { + if (!mL || !(typeof mL === "number") || mL <= 0 ) mL = Infinity + this._max = mL + if (this._length > this._max) trim(this) + } + , get : function () { return this._max } + , enumerable : true + }) + +// resize the cache when the lengthCalculator changes. +Object.defineProperty(LRUCache.prototype, "lengthCalculator", + { set : function (lC) { + if (typeof lC !== "function") { + this._lengthCalculator = naiveLength + this._length = this._itemCount + for (var key in this._cache) { + this._cache[key].length = 1 + } + } else { + this._lengthCalculator = lC + this._length = 0 + for (var key in this._cache) { + this._cache[key].length = this._lengthCalculator(this._cache[key].value) + this._length += this._cache[key].length + } + } + + if (this._length > this._max) trim(this) + } + , get : function () { return this._lengthCalculator } + , enumerable : true + }) + +Object.defineProperty(LRUCache.prototype, "length", + { get : function () { return this._length } + , enumerable : true + }) + + +Object.defineProperty(LRUCache.prototype, "itemCount", + { get : function () { return this._itemCount } + , enumerable : true + }) + +LRUCache.prototype.forEach = function (fn, thisp) { + thisp = thisp || this + var i = 0 + var itemCount = this._itemCount + + for (var k = this._mru - 1; k >= 0 && i < itemCount; k--) if (this._lruList[k]) { + i++ + var hit = this._lruList[k] + if (isStale(this, hit)) { + del(this, hit) + if (!this._allowStale) hit = undefined + } + if (hit) { + fn.call(thisp, hit.value, hit.key, this) + } + } +} + +LRUCache.prototype.keys = function () { + var keys = new Array(this._itemCount) + var i = 0 + for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { + var hit = this._lruList[k] + keys[i++] = hit.key + } + return keys +} + +LRUCache.prototype.values = function () { + var values = new Array(this._itemCount) + var i = 0 + for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { + var hit = this._lruList[k] + values[i++] = hit.value + } + return values +} + +LRUCache.prototype.reset = function () { + if (this._dispose && this._cache) { + for (var k in this._cache) { + this._dispose(k, this._cache[k].value) + } + } + + this._cache = Object.create(null) // hash of items by key + this._lruList = Object.create(null) // list of items in order of use recency + this._mru = 0 // most recently used + this._lru = 0 // least recently used + this._length = 0 // number of items in the list + this._itemCount = 0 +} + +LRUCache.prototype.dump = function () { + var arr = [] + var i = 0 + + for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) { + var hit = this._lruList[k] + if (!isStale(this, hit)) { + //Do not store staled hits + ++i + arr.push({ + k: hit.key, + v: hit.value, + e: hit.now + (hit.maxAge || 0) + }); + } + } + //arr has the most read first + return arr +} + +LRUCache.prototype.dumpLru = function () { + return this._lruList +} + +LRUCache.prototype.set = function (key, value, maxAge) { + maxAge = maxAge || this._maxAge + typeCheckKey(key) + + var now = maxAge ? Date.now() : 0 + var len = this._lengthCalculator(value) + + if (hOP(this._cache, key)) { + if (len > this._max) { + del(this, this._cache[key]) + return false + } + // dispose of the old one before overwriting + if (this._dispose) + this._dispose(key, this._cache[key].value) + + this._cache[key].now = now + this._cache[key].maxAge = maxAge + this._cache[key].value = value + this._length += (len - this._cache[key].length) + this._cache[key].length = len + this.get(key) + + if (this._length > this._max) + trim(this) + + return true + } + + var hit = new Entry(key, value, this._mru++, len, now, maxAge) + + // oversized objects fall out of cache automatically. + if (hit.length > this._max) { + if (this._dispose) this._dispose(key, value) + return false + } + + this._length += hit.length + this._lruList[hit.lu] = this._cache[key] = hit + this._itemCount ++ + + if (this._length > this._max) + trim(this) + + return true +} + +LRUCache.prototype.has = function (key) { + typeCheckKey(key) + if (!hOP(this._cache, key)) return false + var hit = this._cache[key] + if (isStale(this, hit)) { + return false + } + return true +} + +LRUCache.prototype.get = function (key) { + typeCheckKey(key) + return get(this, key, true) +} + +LRUCache.prototype.peek = function (key) { + typeCheckKey(key) + return get(this, key, false) +} + +LRUCache.prototype.pop = function () { + var hit = this._lruList[this._lru] + del(this, hit) + return hit || null +} + +LRUCache.prototype.del = function (key) { + typeCheckKey(key) + del(this, this._cache[key]) +} + +LRUCache.prototype.load = function (arr) { + //reset the cache + this.reset(); + + var now = Date.now() + //A previous serialized cache has the most recent items first + for (var l = arr.length - 1; l >= 0; l-- ) { + var hit = arr[l] + typeCheckKey(hit.k) + var expiresAt = hit.e || 0 + if (expiresAt === 0) { + //the item was created without expiration in a non aged cache + this.set(hit.k, hit.v) + } else { + var maxAge = expiresAt - now + //dont add already expired items + if (maxAge > 0) this.set(hit.k, hit.v, maxAge) + } + } +} + +function get (self, key, doUse) { + typeCheckKey(key) + var hit = self._cache[key] + if (hit) { + if (isStale(self, hit)) { + del(self, hit) + if (!self._allowStale) hit = undefined + } else { + if (doUse) use(self, hit) + } + if (hit) hit = hit.value + } + return hit +} + +function isStale(self, hit) { + if (!hit || (!hit.maxAge && !self._maxAge)) return false + var stale = false; + var diff = Date.now() - hit.now + if (hit.maxAge) { + stale = diff > hit.maxAge + } else { + stale = self._maxAge && (diff > self._maxAge) + } + return stale; +} + +function use (self, hit) { + shiftLU(self, hit) + hit.lu = self._mru ++ + self._lruList[hit.lu] = hit +} + +function trim (self) { + while (self._lru < self._mru && self._length > self._max) + del(self, self._lruList[self._lru]) +} + +function shiftLU (self, hit) { + delete self._lruList[ hit.lu ] + while (self._lru < self._mru && !self._lruList[self._lru]) self._lru ++ +} + +function del (self, hit) { + if (hit) { + if (self._dispose) self._dispose(hit.key, hit.value) + self._length -= hit.length + self._itemCount -- + delete self._cache[ hit.key ] + shiftLU(self, hit) + } +} + +// classy, since V8 prefers predictable objects. +function Entry (key, value, lu, length, now, maxAge) { + this.key = key + this.value = value + this.lu = lu + this.length = length + this.now = now + if (maxAge) this.maxAge = maxAge +} + +})() + +},{}],27:[function(require,module,exports){ +/*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + */ + +/*global define: false Mustache: true*/ + +(function defineMustache (global, factory) { + if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { + factory(exports); // CommonJS + } else if (typeof define === 'function' && define.amd) { + define(['exports'], factory); // AMD + } else { + global.Mustache = {}; + factory(Mustache); // script, wsh, asp + } +}(this, function mustacheFactory (mustache) { + + var objectToString = Object.prototype.toString; + var isArray = Array.isArray || function isArrayPolyfill (object) { + return objectToString.call(object) === '[object Array]'; + }; + + function isFunction (object) { + return typeof object === 'function'; + } + + /** + * More correct typeof string handling array + * which normally returns typeof 'object' + */ + function typeStr (obj) { + return isArray(obj) ? 'array' : typeof obj; + } + + function escapeRegExp (string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); + } + + /** + * Null safe way of checking whether or not an object, + * including its prototype, has a given property + */ + function hasProperty (obj, propName) { + return obj != null && typeof obj === 'object' && (propName in obj); + } + + // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 + // See https://github.com/janl/mustache.js/issues/189 + var regExpTest = RegExp.prototype.test; + function testRegExp (re, string) { + return regExpTest.call(re, string); + } + + var nonSpaceRe = /\S/; + function isWhitespace (string) { + return !testRegExp(nonSpaceRe, string); + } + + var entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + }; + + function escapeHtml (string) { + return String(string).replace(/[&<>"'\/]/g, function fromEntityMap (s) { + return entityMap[s]; + }); + } + + var whiteRe = /\s*/; + var spaceRe = /\s+/; + var equalsRe = /\s*=/; + var curlyRe = /\s*\}/; + var tagRe = /#|\^|\/|>|\{|&|=|!/; + + /** + * Breaks up the given `template` string into a tree of tokens. If the `tags` + * argument is given here it must be an array with two string values: the + * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of + * course, the default is to use mustaches (i.e. mustache.tags). + * + * A token is an array with at least 4 elements. The first element is the + * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag + * did not contain a symbol (i.e. {{myValue}}) this element is "name". For + * all text that appears outside a symbol this element is "text". + * + * The second element of a token is its "value". For mustache tags this is + * whatever else was inside the tag besides the opening symbol. For text tokens + * this is the text itself. + * + * The third and fourth elements of the token are the start and end indices, + * respectively, of the token in the original template. + * + * Tokens that are the root node of a subtree contain two more elements: 1) an + * array of tokens in the subtree and 2) the index in the original template at + * which the closing tag for that section begins. + */ + function parseTemplate (template, tags) { + if (!template) + return []; + + var sections = []; // Stack to hold section tokens + var tokens = []; // Buffer to hold the tokens + var spaces = []; // Indices of whitespace tokens on the current line + var hasTag = false; // Is there a {{tag}} on the current line? + var nonSpace = false; // Is there a non-space char on the current line? + + // Strips all whitespace tokens array for the current line + // if there was a {{#tag}} on it and otherwise only space. + function stripSpace () { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } + + hasTag = false; + nonSpace = false; + } + + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags (tagsToCompile) { + if (typeof tagsToCompile === 'string') + tagsToCompile = tagsToCompile.split(spaceRe, 2); + + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error('Invalid tags: ' + tagsToCompile); + + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); + closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); + } + + compileTags(tags || mustache.tags); + + var scanner = new Scanner(template); + + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; + + // Match any text between tags. + value = scanner.scanUntil(openingTagRe); + + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); + + if (isWhitespace(chr)) { + spaces.push(tokens.length); + } else { + nonSpace = true; + } + + tokens.push([ 'text', chr, start, start + 1 ]); + start += 1; + + // Check for whitespace on the current line. + if (chr === '\n') + stripSpace(); + } + } + + // Match the opening tag. + if (!scanner.scan(openingTagRe)) + break; + + hasTag = true; + + // Get the tag type. + type = scanner.scan(tagRe) || 'name'; + scanner.scan(whiteRe); + + // Get the tag value. + if (type === '=') { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === '{') { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = '&'; + } else { + value = scanner.scanUntil(closingTagRe); + } + + // Match the closing tag. + if (!scanner.scan(closingTagRe)) + throw new Error('Unclosed tag at ' + scanner.pos); + + token = [ type, value, start, scanner.pos ]; + tokens.push(token); + + if (type === '#' || type === '^') { + sections.push(token); + } else if (type === '/') { + // Check section nesting. + openSection = sections.pop(); + + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === 'name' || type === '{' || type === '&') { + nonSpace = true; + } else if (type === '=') { + // Set the tags for the next time around. + compileTags(value); + } + } + + // Make sure there are no open sections when we're done. + openSection = sections.pop(); + + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + + return nestTokens(squashTokens(tokens)); + } + + /** + * Combines the values of consecutive text tokens in the given `tokens` array + * to a single token. + */ + function squashTokens (tokens) { + var squashedTokens = []; + + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + if (token) { + if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + squashedTokens.push(token); + lastToken = token; + } + } + } + + return squashedTokens; + } + + /** + * Forms the given array of `tokens` into a nested tree structure where + * tokens that represent a section have two additional items: 1) an array of + * all tokens that appear in that section and 2) the index in the original + * template that represents the end of that section. + */ + function nestTokens (tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + + switch (token[0]) { + case '#': + case '^': + collector.push(token); + sections.push(token); + collector = token[4] = []; + break; + case '/': + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); + } + } + + return nestedTokens; + } + + /** + * A simple string scanner that is used by the template parser to find + * tokens in template strings. + */ + function Scanner (string) { + this.string = string; + this.tail = string; + this.pos = 0; + } + + /** + * Returns `true` if the tail is empty (end of string). + */ + Scanner.prototype.eos = function eos () { + return this.tail === ''; + }; + + /** + * Tries to match the given regular expression at the current position. + * Returns the matched text if it can match, the empty string otherwise. + */ + Scanner.prototype.scan = function scan (re) { + var match = this.tail.match(re); + + if (!match || match.index !== 0) + return ''; + + var string = match[0]; + + this.tail = this.tail.substring(string.length); + this.pos += string.length; + + return string; + }; + + /** + * Skips all text until the given regular expression can be matched. Returns + * the skipped string, which is the entire tail if no match can be made. + */ + Scanner.prototype.scanUntil = function scanUntil (re) { + var index = this.tail.search(re), match; + + switch (index) { + case -1: + match = this.tail; + this.tail = ''; + break; + case 0: + match = ''; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); + } + + this.pos += match.length; + + return match; + }; + + /** + * Represents a rendering context by wrapping a view object and + * maintaining a reference to the parent context. + */ + function Context (view, parentContext) { + this.view = view; + this.cache = { '.': this.view }; + this.parent = parentContext; + } + + /** + * Creates a new context using the given view with this context + * as the parent. + */ + Context.prototype.push = function push (view) { + return new Context(view, this); + }; + + /** + * Returns the value of the given name in this context, traversing + * up the context hierarchy if the value is absent in this context's view. + */ + Context.prototype.lookup = function lookup (name) { + var cache = this.cache; + + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, names, index, lookupHit = false; + + while (context) { + if (name.indexOf('.') > 0) { + value = context.view; + names = name.split('.'); + index = 0; + + /** + * Using the dot notion path in `name`, we descend through the + * nested objects. + * + * To be certain that the lookup has been successful, we have to + * check if the last object in the path actually has the property + * we are looking for. We store the result in `lookupHit`. + * + * This is specially necessary for when the value has been set to + * `undefined` and we want to avoid looking up parent contexts. + **/ + while (value != null && index < names.length) { + if (index === names.length - 1) + lookupHit = hasProperty(value, names[index]); + + value = value[names[index++]]; + } + } else { + value = context.view[name]; + lookupHit = hasProperty(context.view, name); + } + + if (lookupHit) + break; + + context = context.parent; + } + + cache[name] = value; + } + + if (isFunction(value)) + value = value.call(this.view); + + return value; + }; + + /** + * A Writer knows how to take a stream of tokens and render them to a + * string, given a context. It also maintains a cache of templates to + * avoid the need to parse the same template twice. + */ + function Writer () { + this.cache = {}; + } + + /** + * Clears all cached templates in this writer. + */ + Writer.prototype.clearCache = function clearCache () { + this.cache = {}; + }; + + /** + * Parses and caches the given `template` and returns the array of tokens + * that is generated from the parse. + */ + Writer.prototype.parse = function parse (template, tags) { + var cache = this.cache; + var tokens = cache[template]; + + if (tokens == null) + tokens = cache[template] = parseTemplate(template, tags); + + return tokens; + }; + + /** + * High-level method that is used to render the given `template` with + * the given `view`. + * + * The optional `partials` argument may be an object that contains the + * names and templates of partials that are used in the template. It may + * also be a function that is used to load partial templates on the fly + * that takes a single argument: the name of the partial. + */ + Writer.prototype.render = function render (template, view, partials) { + var tokens = this.parse(template); + var context = (view instanceof Context) ? view : new Context(view); + return this.renderTokens(tokens, context, partials, template); + }; + + /** + * Low-level method that renders the given array of `tokens` using + * the given `context` and `partials`. + * + * Note: The `originalTemplate` is only ever used to extract the portion + * of the original template that was contained in a higher-order section. + * If the template doesn't use higher-order sections, this argument may + * be omitted. + */ + Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) { + var buffer = ''; + + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = undefined; + token = tokens[i]; + symbol = token[0]; + + if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); + else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); + else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate); + else if (symbol === '&') value = this.unescapedValue(token, context); + else if (symbol === 'name') value = this.escapedValue(token, context); + else if (symbol === 'text') value = this.rawValue(token); + + if (value !== undefined) + buffer += value; + } + + return buffer; + }; + + Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { + var self = this; + var buffer = ''; + var value = context.lookup(token[1]); + + // This function is used to render an arbitrary template + // in the current context by higher-order sections. + function subRender (template) { + return self.render(template, context, partials); + } + + if (!value) return; + + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); + } + } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); + } else if (isFunction(value)) { + if (typeof originalTemplate !== 'string') + throw new Error('Cannot use higher-order sections without the original template'); + + // Extract the portion of the original template that the section contains. + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); + + if (value != null) + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate); + } + return buffer; + }; + + Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { + var value = context.lookup(token[1]); + + // Use JavaScript's definition of falsy. Include empty arrays. + // See https://github.com/janl/mustache.js/issues/186 + if (!value || (isArray(value) && value.length === 0)) + return this.renderTokens(token[4], context, partials, originalTemplate); + }; + + Writer.prototype.renderPartial = function renderPartial (token, context, partials) { + if (!partials) return; + + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) + return this.renderTokens(this.parse(value), context, partials, value); + }; + + Writer.prototype.unescapedValue = function unescapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; + }; + + Writer.prototype.escapedValue = function escapedValue (token, context) { + var value = context.lookup(token[1]); + if (value != null) + return mustache.escape(value); + }; + + Writer.prototype.rawValue = function rawValue (token) { + return token[1]; + }; + + mustache.name = 'mustache.js'; + mustache.version = '2.2.0'; + mustache.tags = [ '{{', '}}' ]; + + // All high-level mustache.* functions use this writer. + var defaultWriter = new Writer(); + + /** + * Clears all cached templates in the default writer. + */ + mustache.clearCache = function clearCache () { + return defaultWriter.clearCache(); + }; + + /** + * Parses and caches the given template in the default writer and returns the + * array of tokens it contains. Doing this ahead of time avoids the need to + * parse templates on the fly as they are rendered. + */ + mustache.parse = function parse (template, tags) { + return defaultWriter.parse(template, tags); + }; + + /** + * Renders the `template` with the given `view` and `partials` using the + * default writer. + */ + mustache.render = function render (template, view, partials) { + if (typeof template !== 'string') { + throw new TypeError('Invalid template! Template should be a "string" ' + + 'but "' + typeStr(template) + '" was given as the first ' + + 'argument for mustache#render(template, view, partials)'); + } + + return defaultWriter.render(template, view, partials); + }; + + // This is here for backwards compatibility with 0.4.x., + /*eslint-disable */ // eslint wants camel cased function name + mustache.to_html = function to_html (template, view, partials, send) { + /*eslint-enable*/ + + var result = mustache.render(template, view, partials); + + if (isFunction(send)) { + send(result); + } else { + return result; + } + }; + + // Export the escaping function so that the user may override it. + // See https://github.com/janl/mustache.js/issues/244 + mustache.escape = escapeHtml; + + // Export these mainly for testing, but also for advanced usage. + mustache.Scanner = Scanner; + mustache.Context = Context; + mustache.Writer = Writer; + +})); + +},{}],28:[function(require,module,exports){ +/* object-iterators.js - Mini Underscore library + * by Jonathan Eiten + * + * The methods below operate on objects (but not arrays) similarly + * to Underscore (http://underscorejs.org/#collections). + * + * Recommended usage: + * + * var _ = require('object-iterators'); + */ + +'use strict'; + +/** + * @constructor + * @summary Wrap an object for one method. + * @Desc Note that the `new` keyword is not necessary. + * @param {object|null|undefined} object - `null` or `undefined` is treated as an empty plain object. + * @return {Wrapper} The wrapped object. + */ +function Wrapper(object) { + if (object instanceof Wrapper) { + return object; + } + if (!(this instanceof Wrapper)) { + return new Wrapper(object); + } + this.originalValue = object; + this.o = object || {}; +} + +/** + * @name Wrapper.chain + * @summary Wrap an object for a chain of methods. + * @Desc Calls the constructor `Wrapper()` and modifies the wrapper for chaining. + * @param {object} object + * @return {Wrapper} The wrapped object. + */ +Wrapper.chain = function (object) { + var wrapped = Wrapper(object); // eslint-disable-line new-cap + wrapped.chaining = true; + return wrapped; +}; + +Wrapper.prototype = { + /** + * Unwrap an object wrapped with {@link Wrapper.chain|Wrapper.chain()}. + * @return {object|null|undefined} The value originally wrapped by the constructor. + * @memberOf Wrapper.prototype + */ + value: function () { + return this.originalValue; + }, + + /** + * @desc Mimics Underscore's [each](http://underscorejs.org/#each) method: Iterate over the members of the wrapped object, calling `iteratee()` with each. + * @param {function} iteratee - For each member of the wrapped object, this function is called with three arguments: `(value, key, object)`. The return value of this function is undefined; an `.each` loop cannot be broken out of (use {@link Wrapper#find|.find} instead). + * @param {object} [context] - If given, `iteratee` is bound to this object. In other words, this object becomes the `this` value in the calls to `iteratee`. (Otherwise, the `this` value will be the unwrapped object.) + * @return {Wrapper} The wrapped object for chaining. + * @memberOf Wrapper.prototype + */ + each: function (iteratee, context) { + var o = this.o; + context = context || o; + Object.keys(o).forEach(function (key) { + iteratee.call(context, o[key], key, o); + }); + return this; + }, + + /** + * @desc Mimics Underscore's [find](http://underscorejs.org/#find) method: Look through each member of the wrapped object, returning the first one that passes a truth test (predicate), or `undefined` if no value passes the test. The function returns the value of the first acceptable member, and doesn't necessarily traverse the entire object. + * @param {function} predicate - For each member of the wrapped object, this function is called with three arguments: `(value, key, object)`. The return value of this function should be truthy if the member passes the test and falsy otherwise. + * @param {object} [context] - If given, `predicate` is bound to this object. In other words, this object becomes the `this` value in the calls to `predicate`. (Otherwise, the `this` value will be the unwrapped object.) + * @return {*} The found property's value. + * @memberOf Wrapper.prototype + */ + find: function (predicate, context) { + var o = this.o; + context = context || o; + var result = Object.keys(o).find(function (key) { + return predicate.call(context, o[key], key, o); + }); + return result === undefined ? undefined : o[result]; + }, + + /** + * @desc Mimics Underscore's [reduce](http://underscorejs.org/#reduce) method: Boil down the values of all the members of the wrapped object into a single value. `memo` is the initial state of the reduction, and each successive step of it should be returned by `iteratee()`. + * @param {function} iteratee - For each member of the wrapped object, this function is called with four arguments: `(memo, value, key, object)`. The return value of this function becomes the new value of `memo` for the next iteration. + * @param {*} [memo] - If no memo is passed to the initial invocation of reduce, the iteratee is not invoked on the first element of the list. The first element is instead passed as the memo in the invocation of the iteratee on the next element in the list. + * @param {object} [context] - If given, `iteratee` is bound to this object. In other words, this object becomes the `this` value in the calls to `iteratee`. (Otherwise, the `this` value will be the unwrapped object.) + * @return {*} The value of `memo` "reduced" as per `iteratee`. + * @memberOf Wrapper.prototype + */ + reduce: function (iteratee, memo, context) { + var o = this.o; + context = context || o; + Object.keys(o).forEach(function (key, idx) { + memo = (!idx && memo === undefined) ? o[key] : iteratee.call(context, memo, o[key], key, o); + }); + return memo; + }, + + /** + * @desc Mimics Underscore's [extend](http://underscorejs.org/#extend) method: Copy all of the properties in each of the `source` object parameter(s) over to the (wrapped) destination object (thus mutating it). It's in-order, so the properties of the last `source` object will override properties with the same name in previous arguments or in the destination object. + * > This method copies own members as well as members inherited from prototype chain. + * @param {...object|null|undefined} source - Values of `null` or `undefined` are treated as empty plain objects. + * @return {Wrapper|object} The wrapped destination object if chaining is in effect; otherwise the unwrapped destination object. + * @memberOf Wrapper.prototype + */ + extend: function (source) { + var o = this.o; + Array.prototype.slice.call(arguments).forEach(function (object) { + if (object) { + for (var key in object) { + o[key] = object[key]; + } + } + }); + return this.chaining ? this : o; + }, + + /** + * @desc Mimics Underscore's [extendOwn](http://underscorejs.org/#extendOwn) method: Like {@link Wrapper#extend|extend}, but only copies its "own" properties over to the destination object. + * @param {...object|null|undefined} source - Values of `null` or `undefined` are treated as empty plain objects. + * @return {Wrapper|object} The wrapped destination object if chaining is in effect; otherwise the unwrapped destination object. + * @memberOf Wrapper.prototype + */ + extendOwn: function (source) { + var o = this.o; + Array.prototype.slice.call(arguments).forEach(function (object) { + Wrapper(object).each(function (val, key) { // eslint-disable-line new-cap + o[key] = val; + }); + }); + return this.chaining ? this : o; + } +}; + +module.exports = Wrapper; + +},{}],29:[function(require,module,exports){ +'use strict'; + +/* eslint-env node, browser */ + +/** + * Creates a new read-only property and attaches it to the provided context. + * @private + * @param {string} name - Name for new property. + * @param {*} [value] - Value of new property. + */ +function addReadOnlyProperty(name, value) { + Object.defineProperty(this, name, { + value: value, + writable: false, + enumerable: true, + configurable: false + }); +} + +/** + * @constructor Point + * + * @desc This object represents a single point in an abstract 2-dimensional matrix. + * + * The unit of measure is typically pixels. + * (If used to model computer graphics, vertical coordinates are typically measured downwards + * from the top of the window. This convention however is not inherent in this object.) + * + * Note: This object should be instantiated with the `new` keyword. + * + * @param {number} x - the new point's `x` property + * @param {number} y - the new point's `y` property + */ +function Point(x, y) { + + /** + * @name x + * @type {number} + * @summary This point's horizontal coordinate. + * @desc Created upon instantiation by the {@link Point|constructor}. + * @memberOf Point.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'x', Number(x) || 0); + + /** + * @name y + * @type {number} + * @summary This point's vertical coordinate. + * @desc Created upon instantiation by the {@link Point|constructor}. + * @memberOf Point.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'y', Number(y) || 0); + +} + +Point.prototype = { + + /** + * @returns {Point} A new point which is this point's position increased by coordinates of given `offset`. + * @param {Point} offset - Horizontal and vertical values to add to this point's coordinates. + * @memberOf Point.prototype + */ + plus: function(offset) { + return new Point( + this.x + offset.x, + this.y + offset.y + ); + }, + + /** + * @returns {Point} A new point which is this point's position increased by given offsets. + * @param {number} [offsetX=0] - Value to add to this point's horizontal coordinate. + * @param {number} [offsetY=0] - Value to add to this point's horizontal coordinate. + * @memberOf Point.prototype + */ + plusXY: function(offsetX, offsetY) { + return new Point( + this.x + (offsetX || 0), + this.y + (offsetY || 0) + ); + }, + + /** + * @returns {Point} A new point which is this point's position decreased by coordinates of given `offset`. + * @param {Point} offset - Horizontal and vertical values to subtract from this point's coordinates. + * @memberOf Point.prototype + */ + minus: function(offset) { + return new Point( + this.x - offset.x, + this.y - offset.y + ); + }, + + /** + * @returns {Point} A new `Point` positioned to least x and least y of this point and given `offset`. + * @param {Point} point - A point to compare to this point. + * @memberOf Point.prototype + */ + min: function(point) { + return new Point( + Math.min(this.x, point.x), + Math.min(this.y, point.y) + ); + }, + + /** + * @returns {Point} A new `Point` positioned to greatest x and greatest y of this point and given `point`. + * @param {Point} point - A point to compare to this point. + * @memberOf Point.prototype + */ + max: function(point) { + return new Point( + Math.max(this.x, point.x), + Math.max(this.y, point.y) + ); + }, + + /** + * @returns {number} Distance between given `point` and this point using Pythagorean Theorem formula. + * @param {Point} point - A point from which to compute the distance to this point. + * @memberOf Point.prototype + */ + distance: function(point) { + var deltaX = point.x - this.x, + deltaY = point.y - this.y; + + return Math.sqrt( + deltaX * deltaX + + deltaY * deltaY + ); + }, + + /** + * _(Formerly: `equal`.)_ + * @returns {boolean} `true` iff _both_ coordinates of this point are exactly equal to those of given `point`. + * @param {Point} point - A point to compare to this point. + * @memberOf Point.prototype + */ + equals: function(point) { + var result = false; + + if (point) { + result = + this.x === point.x && + this.y === point.y; + } + + return result; + }, + + /** + * @returns {boolean} `true` iff _both_ coordinates of this point are greater than those of given `point`. + * @param {Point} point - A point to compare to this point + * @memberOf Point.prototype + */ + greaterThan: function(point) { + return ( + this.x > point.x && + this.y > point.y + ); + }, + + /** + * @returns {boolean} `true` iff _both_ coordinates of this point are less than those of given `point`. + * @param {Point} point - A point to compare to this point + * @memberOf Point.prototype + */ + lessThan: function(point) { + return ( + this.x < point.x && + this.y < point.y + ); + }, + + /** + * _(Formerly `greaterThanEqualTo`.)_ + * @returns {boolean} `true` iff _both_ coordinates of this point are greater than or equal to those of given `point`. + * @param {Point} point - A point to compare to this point + * @memberOf Point.prototype + */ + greaterThanOrEqualTo: function(point) { + return ( + this.x >= point.x && + this.y >= point.y + ); + }, + + /** + * _(Formerly `lessThanEqualTo`.)_ + * @returns {boolean} `true` iff _both_ coordinates of this point are less than or equal to those of given `point`. + * @param {Point} point - A point to compare to this point. + * @memberOf Point.prototype + */ + lessThanOrEqualTo: function(point) { + return ( + this.x <= point.x && + this.y <= point.y + ); + }, + + /** + * _(Formerly `isContainedWithinRectangle`.)_ + * @param rect {Rectangle} - Rectangle to test this point against. + * @returns {boolean} `true` iff this point is within given `rect`. + * @memberOf Point.prototype + */ + within: function(rect) { + var minX = rect.origin.x, + maxX = minX + rect.extent.x; + var minY = rect.origin.y, + maxY = minY + rect.extent.y; + + if (rect.extent.x < 0) { + minX = maxX; + maxX = rect.origin.x; + } + + if (rect.extent.y < 0) { + minY = maxY; + maxY = rect.origin.y; + } + + return ( + minX <= this.x && this.x < maxX && + minY <= this.y && this.y < maxY + ); + } +}; + +Point.prototype.EQ = Point.prototype.equals; +Point.prototype.GT = Point.prototype.greaterThan; +Point.prototype.LT = Point.prototype.lessThan; +Point.prototype.GE = Point.prototype.greaterThanOrEqualTo; +Point.prototype.LE = Point.prototype.lessThanOrEqualTo; + + +/** + * @constructor Rectangle + * + * @desc This object represents a rectangular area within an abstract 2-dimensional matrix. + * + * The unit of measure is typically pixels. + * (If used to model computer graphics, vertical coordinates are typically measured downwards + * from the top of the window. This convention however is not inherent in this object.) + * + * Normally, the `x` and `y` parameters to the constructor describe the upper left corner of the rect. + * However, negative values of `width` and `height` will be added to the given `x` and `y`. That is, + * a negative value of the `width` parameter will extend the rect to the left of the given `x` and + * a negative value of the `height` parameter will extend the rect above the given `y`. + * In any case, after instantiation the following are guaranteed to always be true: + * * The `extent`, `width`, and `height` properties _always_ give positive values. + * * The `origin`, `top`, and `left` properties _always_ reflect the upper left corner. + * * The `corner`, `bottom`, and `right` properties _always_ reflect the lower right corner. + * + * Note: This object should be instantiated with the `new` keyword. + * + * @param {number} [x=0] - Horizontal coordinate of some corner of the rect. + * @param {number} [y=0] - Vertical coordinate of some corner of the rect. + * @param {number} [width=0] - Width of the new rect. May be negative (see above). + * @param {number} [height=0] - Height of the new rect. May be negative (see above). + */ +function Rectangle(x, y, width, height) { + + x = Number(x) || 0; + y = Number(y) || 0; + width = Number(width) || 0; + height = Number(height) || 0; + + if (width < 0) { + x += width; + width = -width; + } + + if (height < 0) { + y += height; + height = -height; + } + + /** + * @name origin + * @type {Point} + * @summary Upper left corner of this rect. + * @desc Created upon instantiation by the {@linkplain Rectangle|constructor}. + * @memberOf Rectangle.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'origin', new Point(x, y)); + + /** + * @name extent + * @type {Point} + * @summary this rect's width and height. + * @desc Unlike the other `Point` properties, `extent` is not a global coordinate pair; rather it consists of a _width_ (`x`, always positive) and a _height_ (`y`, always positive). + * + * This object might be more legitimately typed as something like `Area` with properties `width` and `height`; however we wanted it to be able to use it efficiently with a point's `plus` and `minus` methods (that is, without those methods having to check and branch on the type of its parameter). + * + * Created upon instantiation by the {@linkplain Rectangle|constructor}. + * @see The {@link Rectangle#corner|corner} method. + * @memberOf Rectangle.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'extent', new Point(width, height)); + + /** + * @name corner + * @type {Point} + * @summary Lower right corner of this rect. + * @desc This is a calculated value created upon instantiation by the {@linkplain Rectangle|constructor}. It is `origin` offset by `extent`. + * + * **Note:** These coordinates actually point to the pixel one below and one to the right of the rect's actual lower right pixel. + * @memberOf Rectangle.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'corner', new Point(x + width, y + height)); + + /** + * @name center + * @type {Point} + * @summary Center of this rect. + * @desc Created upon instantiation by the {@linkplain Rectangle|constructor}. + * @memberOf Rectangle.prototype + * @abstract + */ + addReadOnlyProperty.call(this, 'center', new Point(x + (width / 2), y + (height / 2))); + +} + +Rectangle.prototype = { + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Minimum vertical coordinate of this rect. + * @memberOf Rectangle.prototype + */ + get top() { + return this.origin.y; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Minimum horizontal coordinate of this rect. + * @memberOf Rectangle.prototype + */ + get left() { + return this.origin.x; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Maximum vertical coordinate of this rect + 1. + * @memberOf Rectangle.prototype + */ + get bottom() { + return this.corner.y; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Maximum horizontal coordinate of this rect + 1. + * @memberOf Rectangle.prototype + */ + get right() { + return this.corner.x; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Width of this rect (always positive). + * @memberOf Rectangle.prototype + */ + get width() { + return this.extent.x; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Height of this rect (always positive). + * @memberOf Rectangle.prototype + */ + get height() { + return this.extent.y; + }, + + /** + * @type {number} + * @desc _(Formerly a function; now a getter.)_ + * @summary Area of this rect. + * @memberOf Rectangle.prototype + */ + get area() { + return this.width * this.height; + }, + + /** + * @returns {Rectangle} A copy of this rect but with horizontal position reset to given `x` and no width. + * @param {number} x - Horizontal coordinate of the new rect. + * @memberOf Rectangle.prototype + */ + flattenXAt: function(x) { + return new Rectangle(x, this.origin.y, 0, this.extent.y); + }, + + /** + * @returns {Rectangle} A copy of this rect but with vertical position reset to given `y` and no height. + * @param {number} y - Vertical coordinate of the new rect. + * @memberOf Rectangle.prototype + */ + flattenYAt: function(y) { + return new Rectangle(this.origin.x, y, this.extent.x, 0); + }, + + /** + * @returns {boolean} `true` iff given `point` entirely contained within this rect. + * @param {Point} pointOrRect - The point or rect to test for containment. + * @memberOf Rectangle.prototype + */ + contains: function(pointOrRect) { + return pointOrRect.within(this); + }, + + /** + * _(Formerly `isContainedWithinRectangle`.)_ + * @returns {boolean} `true` iff `this` rect is entirely contained within given `rect`. + * @param {Rectangle} rect - Rectangle to test against this rect. + * @memberOf Rectangle.prototype + */ + within: function(rect) { + return ( + rect.origin.lessThanOrEqualTo(this.origin) && + rect.corner.greaterThanOrEqualTo(this.corner) + ); + }, + + /** + * _(Formerly: `insetBy`.)_ + * @returns {Rectangle} That is enlarged/shrunk by given `padding`. + * @param {number} padding - Amount by which to increase (+) or decrease (-) this rect + * @see The {@link Rectangle#shrinkBy|shrinkBy} method. + * @memberOf Rectangle.prototype + */ + growBy: function(padding) { + return new Rectangle( + this.origin.x + padding, + this.origin.y + padding, + this.extent.x - padding - padding, + this.extent.y - padding - padding); + }, + + /** + * @returns {Rectangle} That is enlarged/shrunk by given `padding`. + * @param {number} padding - Amount by which to decrease (+) or increase (-) this rect. + * @see The {@link Rectangle#growBy|growBy} method. + * @memberOf Rectangle.prototype + */ + shrinkBy: function(padding) { + return this.growBy(-padding); + }, + + /** + * @returns {Rectangle} Bounding rect that contains both this rect and the given `rect`. + * @param {Rectangle} rect - The rectangle to union with this rect. + * @memberOf Rectangle.prototype + */ + union: function(rect) { + var origin = this.origin.min(rect.origin), + corner = this.corner.max(rect.corner), + extent = corner.minus(origin); + + return new Rectangle( + origin.x, origin.y, + extent.x, extent.y + ); + }, + + /** + * iterate over all points within this rect, invoking `iteratee` for each. + * @param {function(number,number)} iteratee - Function to call for each point. + * Bound to `context` when given; otherwise it is bound to this rect. + * Each invocation of `iteratee` is called with two arguments: + * the horizontal and vertical coordinates of the point. + * @param {object} [context=this] - Context to bind to `iteratee` (when not `this`). + * @memberOf Rectangle.prototype + */ + forEach: function(iteratee, context) { + context = context || this; + for (var x = this.origin.x, x2 = this.corner.x; x < x2; x++) { + for (var y = this.origin.y, y2 = this.corner.y; y < y2; y++) { + iteratee.call(context, x, y); + } + } + }, + + /** + * @returns {Rectangle} One of: + * * _If this rect intersects with the given `rect`:_ + * a new rect representing that intersection. + * * _If it doesn't intersect and `ifNoneAction` defined:_ + * result of calling `ifNoneAction`. + * * _If it doesn't intersect and `ifNoneAction` undefined:_ + * `null`. + * @param {Rectangle} rect - The rectangle to intersect with this rect. + * @param {function(Rectangle)} [ifNoneAction] - When no intersection, invoke and return result. + * Bound to `context` when given; otherwise bound to this rect. + * Invoked with `rect` as sole parameter. + * @param {object} [context=this] - Context to bind to `ifNoneAction` (when not `this`). + * @memberOf Rectangle.prototype + */ + intersect: function(rect, ifNoneAction, context) { + var result = null, + origin = this.origin.max(rect.origin), + corner = this.corner.min(rect.corner), + extent = corner.minus(origin); + + if (extent.x > 0 && extent.y > 0) { + result = new Rectangle( + origin.x, origin.y, + extent.x, extent.y + ); + } else if (typeof ifNoneAction === 'function') { + result = ifNoneAction.call(context || this, rect); + } + + return result; + }, + + /** + * @returns {boolean} `true` iff this rect overlaps with given `rect`. + * @param {Rectangle} rect - The rectangle to intersect with this rect. + * @memberOf Rectangle.prototype + */ + intersects: function(rect) { + return ( + rect.corner.x > this.origin.x && + rect.corner.y > this.origin.y && + rect.origin.x < this.corner.x && + rect.origin.y < this.corner.y + ); + } +}; + +// Interface +exports.Point = Point; +exports.Rectangle = Rectangle; + +},{}],30:[function(require,module,exports){ +'use strict'; + +/* eslint-env node, browser */ + +(function (module) { // eslint-disable-line no-unused-expressions + + // This closure supports NodeJS-less client side includes with + + + + + + + + + +
          +
          +
          + + + + + + + + + +
          +
          +
          +
          +
          +
          +
          +
          +
          + blah +
          +
          +
          + + + + + diff --git a/examples/autosizeissue/js/accounting.min.js b/examples/autosizeissue/js/accounting.min.js new file mode 100755 index 000000000..0fc755d38 --- /dev/null +++ b/examples/autosizeissue/js/accounting.min.js @@ -0,0 +1,4 @@ +/*! + * accounting.js v0.3.2, copyright 2011 Joss Crowcroft, MIT license, http://josscrowcroft.github.com/accounting.js + */ +(function(p,z){function q(a){return!!(""===a||a&&a.charCodeAt&&a.substr)}function m(a){return u?u(a):"[object Array]"===v.call(a)}function r(a){return"[object Object]"===v.call(a)}function s(a,b){var d,a=a||{},b=b||{};for(d in b)b.hasOwnProperty(d)&&null==a[d]&&(a[d]=b[d]);return a}function j(a,b,d){var c=[],e,h;if(!a)return c;if(w&&a.map===w)return a.map(b,d);for(e=0,h=a.length;ea?"-":"",g=parseInt(y(Math.abs(a||0),h),10)+"",l=3a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal))};c.formatColumn=function(a,b,d,i,e,h){if(!a)return[];var f=s(r(b)?b:{symbol:b,precision:d,thousand:i,decimal:e,format:h},c.settings.currency),g=x(f.format),l=g.pos.indexOf("%s")a?g.neg:g.zero).replace("%s",f.symbol).replace("%v",t(Math.abs(a),n(f.precision),f.thousand,f.decimal));if(a.length>k)k=a.length;return a});return j(a,function(a){return q(a)&&a.length>>>>>> 90126663236fe66595178ba134f71205a528c57d "1-abs-down": { type: "image/png", data: "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAKCAYAAACE2W/HAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC41ZYUyZQAAAFFJREFUKFNjQAL/oTTD////CWJkgFMjEAgD8Q4gLkMSgwOsGoGgDCQExcRrRFJImo1ICqmnEUSiYJgkMgYCrDYia8TQBFVIJ6cCAXJ0QDGDDQD67OYX9wdp0wAAAABJRU5ErkJggg==" @@ -151,93 +155,81 @@ module.exports = { // This file generated by gulp-imagine-64 at 11:56:56 AM on 1 /* eslint-env browser */ -(function (module) { // eslint-disable-line no-unused-expressions - - // This closure supports NodeJS-less client side includes with