survey-analytics
Version:
SurveyJS analytics Library.
1,745 lines (1,495 loc) • 597 kB
JavaScript
/*!
* surveyjs - SurveyJS Dashboard library v2.0.5
* Copyright (c) 2015-2025 Devsoft Baltic OÜ - http://surveyjs.io/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("plotly.js-dist-min"), require("survey-core"));
else if(typeof define === 'function' && define.amd)
define("SurveyAnalytics", ["plotly.js-dist-min", "survey-core"], factory);
else if(typeof exports === 'object')
exports["SurveyAnalytics"] = factory(require("plotly.js-dist-min"), require("survey-core"));
else
root["SurveyAnalytics"] = factory(root["Plotly"], root["Survey"]);
})(this, (__WEBPACK_EXTERNAL_MODULE_plotly_js_dist_min__, __WEBPACK_EXTERNAL_MODULE_survey_core__) => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./node_modules/muuri/dist/muuri.js":
/*!******************************************!*\
!*** ./node_modules/muuri/dist/muuri.js ***!
\******************************************/
/***/ (function(module) {
/**
* Muuri v0.8.0
* https://github.com/haltu/muuri
* Copyright (c) 2015-present, Haltu Oy
* Released under the MIT license
* https://github.com/haltu/muuri/blob/master/LICENSE.md
* @license MIT
*
* Muuri Packer
* Copyright (c) 2016-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*
* Muuri Ticker / Muuri Emitter / Muuri Queue
* Copyright (c) 2018-present, Niklas Rämö <inramo@gmail.com>
* @license MIT
*/
(function (global, factory) {
true ? module.exports = factory() :
0;
}(this, function () { 'use strict';
var namespace = 'Muuri';
var gridInstances = {};
var actionSwap = 'swap';
var actionMove = 'move';
var eventSynchronize = 'synchronize';
var eventLayoutStart = 'layoutStart';
var eventLayoutEnd = 'layoutEnd';
var eventAdd = 'add';
var eventRemove = 'remove';
var eventShowStart = 'showStart';
var eventShowEnd = 'showEnd';
var eventHideStart = 'hideStart';
var eventHideEnd = 'hideEnd';
var eventFilter = 'filter';
var eventSort = 'sort';
var eventMove = 'move';
var eventSend = 'send';
var eventBeforeSend = 'beforeSend';
var eventReceive = 'receive';
var eventBeforeReceive = 'beforeReceive';
var eventDragInit = 'dragInit';
var eventDragStart = 'dragStart';
var eventDragMove = 'dragMove';
var eventDragScroll = 'dragScroll';
var eventDragEnd = 'dragEnd';
var eventDragReleaseStart = 'dragReleaseStart';
var eventDragReleaseEnd = 'dragReleaseEnd';
var eventDestroy = 'destroy';
/**
* Event emitter constructor.
*
* @class
*/
function Emitter() {
this._events = {};
this._queue = [];
this._counter = 0;
this._isDestroyed = false;
}
/**
* Public prototype methods
* ************************
*/
/**
* Bind an event listener.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {Function} listener
* @returns {Emitter}
*/
Emitter.prototype.on = function(event, listener) {
if (this._isDestroyed) return this;
// Get listeners queue and create it if it does not exist.
var listeners = this._events[event];
if (!listeners) listeners = this._events[event] = [];
// Add the listener to the queue.
listeners.push(listener);
return this;
};
/**
* Unbind all event listeners that match the provided listener function.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {Function} [listener]
* @returns {Emitter}
*/
Emitter.prototype.off = function(event, listener) {
if (this._isDestroyed) return this;
// Get listeners and return immediately if none is found.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
// If no specific listener is provided remove all listeners.
if (!listener) {
listeners.length = 0;
return this;
}
// Remove all matching listeners.
var i = listeners.length;
while (i--) {
if (listener === listeners[i]) listeners.splice(i, 1);
}
return this;
};
/**
* Emit all listeners in a specified event with the provided arguments.
*
* @public
* @memberof Emitter.prototype
* @param {String} event
* @param {*} [arg1]
* @param {*} [arg2]
* @param {*} [arg3]
* @returns {Emitter}
*/
Emitter.prototype.emit = function(event, arg1, arg2, arg3) {
if (this._isDestroyed) return this;
// Get event listeners and quit early if there's no listeners.
var listeners = this._events[event];
if (!listeners || !listeners.length) return this;
var queue = this._queue;
var qLength = queue.length;
var aLength = arguments.length - 1;
var i;
// Add the current listeners to the callback queue before we process them.
// This is necessary to guarantee that all of the listeners are called in
// correct order even if new event listeners are removed/added during
// processing and/or events are emitted during processing.
for (i = 0; i < listeners.length; i++) {
queue.push(listeners[i]);
}
// Increment queue counter. This is needed for the scenarios where emit is
// triggered while the queue is already processing. We need to keep track of
// how many "queue processors" there are active so that we can safely reset
// the queue in the end when the last queue processor is finished.
++this._counter;
// Process the queue (the specific part of it for this emit).
for (i = qLength, qLength = queue.length; i < qLength; i++) {
// prettier-ignore
aLength === 0 ? queue[i]() :
aLength === 1 ? queue[i](arg1) :
aLength === 2 ? queue[i](arg1, arg2) :
queue[i](arg1, arg2, arg3);
// Stop processing if the emitter is destroyed.
if (this._isDestroyed) return this;
}
// Decrement queue process counter.
--this._counter;
// Reset the queue if there are no more queue processes running.
if (!this._counter) queue.length = 0;
return this;
};
/**
* Destroy emitter instance. Basically just removes all bound listeners.
*
* @public
* @memberof Emitter.prototype
* @returns {Emitter}
*/
Emitter.prototype.destroy = function() {
if (this._isDestroyed) return this;
var events = this._events;
var event;
// Flag as destroyed.
this._isDestroyed = true;
// Reset queue (if queue is currently processing this will also stop that).
this._queue.length = this._counter = 0;
// Remove all listeners.
for (event in events) {
if (events[event]) {
events[event].length = 0;
events[event] = undefined;
}
}
return this;
};
// Set up the default export values.
var transformStyle = 'transform';
var transformProp = 'transform';
// Find the supported transform prop and style names.
var docElemStyle = window.document.documentElement.style;
var style = 'transform';
var styleCap = 'Transform';
var found = false;
['', 'Webkit', 'Moz', 'O', 'ms'].forEach(function(prefix) {
if (found) return;
var propName = prefix ? prefix + styleCap : style;
if (docElemStyle[propName] !== undefined) {
prefix = prefix.toLowerCase();
transformStyle = prefix ? '-' + prefix + '-' + style : style;
transformProp = propName;
found = true;
}
});
var stylesCache = typeof WeakMap === 'function' ? new WeakMap() : null;
/**
* Returns the computed value of an element's style property as a string.
*
* @param {HTMLElement} element
* @param {String} style
* @returns {String}
*/
function getStyle(element, style) {
var styles = stylesCache && stylesCache.get(element);
if (!styles) {
styles = window.getComputedStyle(element, null);
if (stylesCache) stylesCache.set(element, styles);
}
return styles.getPropertyValue(style === 'transform' ? transformStyle : style);
}
var styleNameRegEx = /([A-Z])/g;
/**
* Transforms a camel case style property to kebab case style property.
*
* @param {String} string
* @returns {String}
*/
function getStyleName(string) {
return string.replace(styleNameRegEx, '-$1').toLowerCase();
}
var strFunction = 'function';
/**
* Check if a value is a function.
*
* @param {*} val
* @returns {Boolean}
*/
function isFunction(val) {
return typeof val === strFunction;
}
var transformStyle$1 = 'transform';
/**
* Set inline styles to an element.
*
* @param {HTMLElement} element
* @param {Object} styles
*/
function setStyles(element, styles) {
for (var prop in styles) {
element.style[prop === transformStyle$1 ? transformProp : prop] = styles[prop];
}
}
/**
* Item animation handler powered by Web Animations API.
*
* @class
* @param {HTMLElement} element
*/
function ItemAnimate(element) {
this._element = element;
this._animation = null;
this._callback = null;
this._props = [];
this._values = [];
this._keyframes = [];
this._options = {};
this._isDestroyed = false;
this._onFinish = this._onFinish.bind(this);
}
/**
* Public prototype methods
* ************************
*/
/**
* Start instance's animation. Automatically stops current animation if it is
* running.
*
* @public
* @memberof ItemAnimate.prototype
* @param {Object} propsFrom
* @param {Object} propsTo
* @param {Object} [options]
* @param {Number} [options.duration=300]
* @param {String} [options.easing='ease']
* @param {Function} [options.onFinish]
*/
ItemAnimate.prototype.start = function(propsFrom, propsTo, options) {
if (this._isDestroyed) return;
var animation = this._animation;
var currentProps = this._props;
var currentValues = this._values;
var opts = options || 0;
var cancelAnimation = false;
// If we have an existing animation running, let's check if it needs to be
// cancelled or if it can continue running.
if (animation) {
var propCount = 0;
var propIndex;
// Check if the requested animation target props and values match with the
// current props and values.
for (var propName in propsTo) {
++propCount;
propIndex = currentProps.indexOf(propName);
if (propIndex === -1 || propsTo[propName] !== currentValues[propIndex]) {
cancelAnimation = true;
break;
}
}
// Check if the target props count matches current props count. This is
// needed for the edge case scenario where target props contain the same
// styles as current props, but the current props have some additional
// props.
if (!cancelAnimation && propCount !== currentProps.length) {
cancelAnimation = true;
}
}
// Cancel animation (if required).
if (cancelAnimation) animation.cancel();
// Store animation callback.
this._callback = isFunction(opts.onFinish) ? opts.onFinish : null;
// If we have a running animation that does not need to be cancelled, let's
// call it a day here and let it run.
if (animation && !cancelAnimation) return;
// Store target props and values to instance.
currentProps.length = currentValues.length = 0;
for (propName in propsTo) {
currentProps.push(propName);
currentValues.push(propsTo[propName]);
}
// Set up keyframes.
var animKeyframes = this._keyframes;
animKeyframes[0] = propsFrom;
animKeyframes[1] = propsTo;
// Set up options.
var animOptions = this._options;
animOptions.duration = opts.duration || 300;
animOptions.easing = opts.easing || 'ease';
// Start the animation
var element = this._element;
animation = element.animate(animKeyframes, animOptions);
animation.onfinish = this._onFinish;
this._animation = animation;
// Set the end styles. This makes sure that the element stays at the end
// values after animation is finished.
setStyles(element, propsTo);
};
/**
* Stop instance's current animation if running.
*
* @public
* @memberof ItemAnimate.prototype
* @param {Object} [styles]
*/
ItemAnimate.prototype.stop = function(styles) {
if (this._isDestroyed || !this._animation) return;
var element = this._element;
var currentProps = this._props;
var currentValues = this._values;
var propName;
var propValue;
var i;
// Calculate (if not provided) and set styles.
if (!styles) {
for (i = 0; i < currentProps.length; i++) {
propName = currentProps[i];
propValue = getStyle(element, getStyleName(propName));
element.style[propName === 'transform' ? transformProp : propName] = propValue;
}
} else {
setStyles(element, styles);
}
// Cancel animation.
this._animation.cancel();
this._animation = this._callback = null;
// Reset current props and values.
currentProps.length = currentValues.length = 0;
};
/**
* Check if the item is being animated currently.
*
* @public
* @memberof ItemAnimate.prototype
* @return {Boolean}
*/
ItemAnimate.prototype.isAnimating = function() {
return !!this._animation;
};
/**
* Destroy the instance and stop current animation if it is running.
*
* @public
* @memberof ItemAnimate.prototype
*/
ItemAnimate.prototype.destroy = function() {
if (this._isDestroyed) return;
this.stop();
this._element = this._options = this._keyframes = null;
this._isDestroyed = true;
};
/**
* Private prototype methods
* *************************
*/
/**
* Animation end handler.
*
* @private
* @memberof ItemAnimate.prototype
*/
ItemAnimate.prototype._onFinish = function() {
var callback = this._callback;
this._animation = this._callback = null;
this._props.length = this._values.length = 0;
callback && callback();
};
var vendorPrefixes = ['', 'webkit', 'moz', 'ms', 'o', 'Webkit', 'Moz', 'MS', 'O'];
/**
* Get prefixed CSS property name when given a non-prefixed CSS property name.
* @param {Object} elemStyle
* @param {String} propName
* @returns {!String}
*/
function getPrefixedPropName(elemStyle, propName) {
var camelPropName = propName[0].toUpperCase() + propName.slice(1);
var i = 0;
var prefix;
var prefixedPropName;
while (i < vendorPrefixes.length) {
prefix = vendorPrefixes[i];
prefixedPropName = prefix ? prefix + camelPropName : propName;
if (prefixedPropName in elemStyle) return prefixedPropName;
++i;
}
return null;
}
var dt = 1000 / 60;
var raf = (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
return this.setTimeout(function() {
callback(dt);
}, dt);
}
).bind(window);
// Detect support for passive events:
// https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
var isPassiveEventsSupported = false;
try {
var passiveOpts = Object.defineProperty({}, 'passive', {
get: function() {
isPassiveEventsSupported = true;
}
});
window.addEventListener('testPassive', null, passiveOpts);
window.removeEventListener('testPassive', null, passiveOpts);
} catch (e) {}
// Dragger events.
var events = {
start: 'start',
move: 'move',
end: 'end',
cancel: 'cancel'
};
var hasTouchEvents = !!('ontouchstart' in window || window.TouchEvent);
var hasPointerEvents = !!window.PointerEvent;
var hasMsPointerEvents = !!window.navigator.msPointerEnabled;
var isAndroid = /(android)/i.test(window.navigator.userAgent);
var listenerOptions = isPassiveEventsSupported ? { passive: true } : false;
var taProp = 'touchAction';
var taPropPrefixed = getPrefixedPropName(window.document.documentElement.style, taProp);
var taDefaultValue = 'auto';
/**
* Creates a new Dragger instance for an element.
*
* @public
* @class
* @param {HTMLElement} element
* @param {Object} [cssProps]
*/
function Dragger(element, cssProps) {
this._element = element;
this._emitter = new Emitter();
this._isDestroyed = false;
this._cssProps = {};
this._touchAction = '';
this._startEvent = null;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._preStartCheck = this._preStartCheck.bind(this);
this._abortNonCancelable = this._abortNonCancelable.bind(this);
this._onStart = this._onStart.bind(this);
this._onMove = this._onMove.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onEnd = this._onEnd.bind(this);
// Apply initial css props.
this.setCssProps(cssProps);
// If touch action was not provided with initial css props let's assume it's
// auto.
if (!this._touchAction) {
this.setTouchAction(taDefaultValue);
}
// Prevent native link/image dragging for the item and it's ancestors.
element.addEventListener('dragstart', Dragger._preventDefault, false);
// Listen to start event.
element.addEventListener(Dragger._events.start, this._preStartCheck, listenerOptions);
// If we have touch events, but no pointer events we need to also listen for
// mouse events in addition to touch events for devices which support both
// mouse and touch interaction.
if (hasTouchEvents && !hasPointerEvents && !hasMsPointerEvents) {
element.addEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions);
}
}
/**
* Protected properties
* ********************
*/
Dragger._pointerEvents = {
start: 'pointerdown',
move: 'pointermove',
cancel: 'pointercancel',
end: 'pointerup'
};
Dragger._msPointerEvents = {
start: 'MSPointerDown',
move: 'MSPointerMove',
cancel: 'MSPointerCancel',
end: 'MSPointerUp'
};
Dragger._touchEvents = {
start: 'touchstart',
move: 'touchmove',
cancel: 'touchcancel',
end: 'touchend'
};
Dragger._mouseEvents = {
start: 'mousedown',
move: 'mousemove',
cancel: '',
end: 'mouseup'
};
Dragger._events = (function() {
if (hasPointerEvents) return Dragger._pointerEvents;
if (hasMsPointerEvents) return Dragger._msPointerEvents;
if (hasTouchEvents) return Dragger._touchEvents;
return Dragger._mouseEvents;
})();
Dragger._emitter = new Emitter();
Dragger._activeInstances = [];
/**
* Protected static methods
* ************************
*/
Dragger._preventDefault = function(e) {
if (e.preventDefault && e.cancelable !== false) e.preventDefault();
};
Dragger._activateInstance = function(instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index > -1) return;
Dragger._activeInstances.push(instance);
Dragger._emitter.on(events.move, instance._onMove);
Dragger._emitter.on(events.cancel, instance._onCancel);
Dragger._emitter.on(events.end, instance._onEnd);
if (Dragger._activeInstances.length === 1) {
Dragger._bindListeners();
}
};
Dragger._deactivateInstance = function(instance) {
var index = Dragger._activeInstances.indexOf(instance);
if (index === -1) return;
Dragger._activeInstances.splice(index, 1);
Dragger._emitter.off(events.move, instance._onMove);
Dragger._emitter.off(events.cancel, instance._onCancel);
Dragger._emitter.off(events.end, instance._onEnd);
if (!Dragger._activeInstances.length) {
Dragger._unbindListeners();
}
};
Dragger._bindListeners = function() {
var events = Dragger._events;
window.addEventListener(events.move, Dragger._onMove, listenerOptions);
window.addEventListener(events.end, Dragger._onEnd, listenerOptions);
events.cancel && window.addEventListener(events.cancel, Dragger._onCancel, listenerOptions);
};
Dragger._unbindListeners = function() {
var events = Dragger._events;
window.removeEventListener(events.move, Dragger._onMove, listenerOptions);
window.removeEventListener(events.end, Dragger._onEnd, listenerOptions);
events.cancel && window.removeEventListener(events.cancel, Dragger._onCancel, listenerOptions);
};
Dragger._getEventPointerId = function(event) {
// If we have pointer id available let's use it.
if (typeof event.pointerId === 'number') {
return event.pointerId;
}
// For touch events let's get the first changed touch's identifier.
if (event.changedTouches) {
return event.changedTouches[0] ? event.changedTouches[0].identifier : null;
}
// For mouse/other events let's provide a static id.
return 1;
};
Dragger._getTouchById = function(event, id) {
// If we have a pointer event return the whole event if there's a match, and
// null otherwise.
if (typeof event.pointerId === 'number') {
return event.pointerId === id ? event : null;
}
// For touch events let's check if there's a changed touch object that matches
// the pointerId in which case return the touch object.
if (event.changedTouches) {
for (var i = 0; i < event.changedTouches.length; i++) {
if (event.changedTouches[i].identifier === id) {
return event.changedTouches[i];
}
}
return null;
}
// For mouse/other events let's assume there's only one pointer and just
// return the event.
return event;
};
Dragger._onMove = function(e) {
Dragger._emitter.emit(events.move, e);
};
Dragger._onCancel = function(e) {
Dragger._emitter.emit(events.cancel, e);
};
Dragger._onEnd = function(e) {
Dragger._emitter.emit(events.end, e);
};
/**
* Private prototype methods
* *************************
*/
/**
* Reset current drag operation (if any).
*
* @private
* @memberof Dragger.prototype
*/
Dragger.prototype._reset = function() {
if (this._isDestroyed) return;
this._pointerId = null;
this._startTime = 0;
this._startX = 0;
this._startY = 0;
this._currentX = 0;
this._currentY = 0;
this._startEvent = null;
this._element.removeEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
Dragger._deactivateInstance(this);
};
/**
* Create a custom dragger event from a raw event.
*
* @private
* @memberof Dragger.prototype
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
* @returns {DraggerEvent}
*/
Dragger.prototype._createEvent = function(type, e) {
var touch = this._getTrackedTouch(e);
return {
// Hammer.js compatibility interface.
type: type,
srcEvent: e,
distance: this.getDistance(),
deltaX: this.getDeltaX(),
deltaY: this.getDeltaY(),
deltaTime: type === events.start ? 0 : this.getDeltaTime(),
isFirst: type === events.start,
isFinal: type === events.end || type === events.cancel,
// Partial Touch API interface.
identifier: this._pointerId,
screenX: touch.screenX,
screenY: touch.screenY,
clientX: touch.clientX,
clientY: touch.clientY,
pageX: touch.pageX,
pageY: touch.pageY,
target: touch.target
};
};
/**
* Emit a raw event as dragger event internally.
*
* @private
* @memberof Dragger.prototype
* @param {String} type
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._emit = function(type, e) {
this._emitter.emit(type, this._createEvent(type, e));
};
/**
* If the provided event is a PointerEvent this method will return it if it has
* the same pointerId as the instance. If the provided event is a TouchEvent
* this method will try to look for a Touch instance in the changedTouches that
* has an identifier matching this instance's pointerId. If the provided event
* is a MouseEvent (or just any other event than PointerEvent or TouchEvent)
* it will be returned immediately.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)}
* @returns {?(Touch|PointerEvent|MouseEvent)}
*/
Dragger.prototype._getTrackedTouch = function(e) {
if (this._pointerId === null) {
return null;
} else {
return Dragger._getTouchById(e, this._pointerId);
}
};
/**
* A pre-handler for start event that checks if we can start dragging.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._preStartCheck = function(e) {
if (this._isDestroyed) return;
// Make sure the element is not being dragged currently.
if (this.isDragging()) return;
// Special cancelable check for Android to prevent drag procedure from
// starting if native scrolling is in progress. Part 1.
if (isAndroid && e.cancelable === false) return;
// Make sure left button is pressed on mouse.
if (e.button) return;
// Get (and set) pointer id.
this._pointerId = Dragger._getEventPointerId(e);
if (this._pointerId === null) return;
// Store the start event and trigger start (async or sync). Pointer events
// are emitted before touch events if the browser supports both of them. And
// if you try to move an element before `touchstart` is emitted the pointer
// events for that element will be canceled. The fix is to delay the emitted
// pointer events in such a scenario by one frame so that `touchstart` has
// time to be emitted before the element is (potentially) moved.
this._startEvent = e;
if (hasTouchEvents && (hasPointerEvents || hasMsPointerEvents)) {
// Special cancelable check for Android to prevent drag procedure from
// starting if native scrolling is in progress. Part 2.
if (isAndroid) {
this._element.addEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
}
raf(this._onStart);
} else {
this._onStart();
}
};
/**
* Abort start event if it turns out to be non-cancelable.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._abortNonCancelable = function(e) {
this._element.removeEventListener(
Dragger._touchEvents.start,
this._abortNonCancelable,
listenerOptions
);
if (this._startEvent && e.cancelable === false) {
this._pointerId = null;
this._startEvent = null;
}
};
/**
* Start the drag procedure if possible.
*
* @private
* @memberof Dragger.prototype
*/
Dragger.prototype._onStart = function() {
var e = this._startEvent;
if (!e) return;
this._startEvent = null;
var touch = this._getTrackedTouch(e);
if (!touch) return;
// Set up init data and emit start event.
this._startX = this._currentX = touch.clientX;
this._startY = this._currentY = touch.clientY;
this._startTime = Date.now();
this._emit(events.start, e);
Dragger._activateInstance(this);
};
/**
* Handler for move event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onMove = function(e) {
var touch = this._getTrackedTouch(e);
if (!touch) return;
this._currentX = touch.clientX;
this._currentY = touch.clientY;
this._emit(events.move, e);
};
/**
* Handler for move cancel event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onCancel = function(e) {
if (!this._getTrackedTouch(e)) return;
this._emit(events.cancel, e);
this._reset();
};
/**
* Handler for end event.
*
* @private
* @memberof Dragger.prototype
* @param {(PointerEvent|TouchEvent|MouseEvent)} e
*/
Dragger.prototype._onEnd = function(e) {
if (!this._getTrackedTouch(e)) return;
this._emit(events.end, e);
this._reset();
};
/**
* Public prototype methods
* ************************
*/
/**
* Check if the element is being dragged at the moment.
*
* @public
* @memberof Dragger.prototype
* @returns {Boolean}
*/
Dragger.prototype.isDragging = function() {
return this._pointerId !== null;
};
/**
* Set element's touch-action CSS property.
*
* @public
* @memberof Dragger.prototype
* @param {String} value
*/
Dragger.prototype.setTouchAction = function(value) {
// Store unmodified touch action value (we trust user input here).
this._touchAction = value;
// Set touch-action style.
if (taPropPrefixed) {
this._cssProps[taPropPrefixed] = '';
this._element.style[taPropPrefixed] = value;
}
// If we have an unsupported touch-action value let's add a special listener
// that prevents default action on touch start event. A dirty hack, but best
// we can do for now. The other options would be to somehow polyfill the
// unsupported touch action behavior with custom heuristics which sounds like
// a can of worms.
if (hasTouchEvents) {
this._element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
if (this._element.style[taPropPrefixed] !== value) {
this._element.addEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
}
}
};
/**
* Update element's CSS properties. Accepts an object with camel cased style
* props with value pairs as it's first argument.
*
* @public
* @memberof Dragger.prototype
* @param {Object} [newProps]
*/
Dragger.prototype.setCssProps = function(newProps) {
if (!newProps) return;
var currentProps = this._cssProps;
var element = this._element;
var prop;
var prefixedProp;
// Reset current props.
for (prop in currentProps) {
element.style[prop] = currentProps[prop];
delete currentProps[prop];
}
// Set new props.
for (prop in newProps) {
// Make sure we have a value for the prop.
if (!newProps[prop]) continue;
// Special handling for touch-action.
if (prop === taProp) {
this.setTouchAction(newProps[prop]);
continue;
}
// Get prefixed prop and skip if it does not exist.
prefixedProp = getPrefixedPropName(element.style, prop);
if (!prefixedProp) continue;
// Store the prop and add the style.
currentProps[prefixedProp] = '';
element.style[prefixedProp] = newProps[prop];
}
};
/**
* How much the pointer has moved on x-axis from start position, in pixels.
* Positive value indicates movement from left to right.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaX = function() {
return this._currentX - this._startX;
};
/**
* How much the pointer has moved on y-axis from start position, in pixels.
* Positive value indicates movement from top to bottom.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaY = function() {
return this._currentY - this._startY;
};
/**
* How far (in pixels) has pointer moved from start position.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDistance = function() {
var x = this.getDeltaX();
var y = this.getDeltaY();
return Math.sqrt(x * x + y * y);
};
/**
* How long has pointer been dragged.
*
* @public
* @memberof Dragger.prototype
* @returns {Number}
*/
Dragger.prototype.getDeltaTime = function() {
return this._startTime ? Date.now() - this._startTime : 0;
};
/**
* Bind drag event listeners.
*
* @public
* @memberof Dragger.prototype
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.on = function(eventName, listener) {
this._emitter.on(eventName, listener);
};
/**
* Unbind drag event listeners.
*
* @public
* @memberof Dragger.prototype
* @param {String} eventName
* - 'start', 'move', 'cancel' or 'end'.
* @param {Function} listener
*/
Dragger.prototype.off = function(events, listener) {
this._emitter.off(eventName, listener);
};
/**
* Destroy the instance and unbind all drag event listeners.
*
* @public
* @memberof Dragger.prototype
*/
Dragger.prototype.destroy = function() {
if (this._isDestroyed) return;
var element = this._element;
var events = Dragger._events;
// Reset data and deactivate the instance.
this._reset();
// Destroy emitter.
this._emitter.destroy();
// Unbind event handlers.
element.removeEventListener(events.start, this._preStartCheck, listenerOptions);
element.removeEventListener(Dragger._mouseEvents.start, this._preStartCheck, listenerOptions);
element.removeEventListener('dragstart', Dragger._preventDefault, false);
element.removeEventListener(Dragger._touchEvents.start, Dragger._preventDefault, false);
// Reset styles.
for (var prop in this._cssProps) {
element.style[prop] = this._cssProps[prop];
delete this._cssProps[prop];
}
// Reset data.
this._element = null;
// Mark as destroyed.
this._isDestroyed = true;
};
/**
* A ticker system for handling DOM reads and writes in an efficient way.
* Contains a read queue and a write queue that are processed on the next
* animation frame when needed.
*
* @class
*/
function Ticker() {
this._nextStep = null;
this._queue = [];
this._reads = {};
this._writes = {};
this._batch = [];
this._batchReads = {};
this._batchWrites = {};
this._step = this._step.bind(this);
}
Ticker.prototype.add = function(id, readOperation, writeOperation, isPrioritized) {
// First, let's check if an item has been added to the queues with the same id
// and if so -> remove it.
var currentIndex = this._queue.indexOf(id);
if (currentIndex > -1) this._queue[currentIndex] = undefined;
// Add entry.
isPrioritized ? this._queue.unshift(id) : this._queue.push(id);
this._reads[id] = readOperation;
this._writes[id] = writeOperation;
// Finally, let's kick-start the next tick if it is not running yet.
if (!this._nextStep) this._nextStep = raf(this._step);
};
Ticker.prototype.cancel = function(id) {
var currentIndex = this._queue.indexOf(id);
if (currentIndex > -1) {
this._queue[currentIndex] = undefined;
delete this._reads[id];
delete this._writes[id];
}
};
Ticker.prototype._step = function() {
var queue = this._queue;
var reads = this._reads;
var writes = this._writes;
var batch = this._batch;
var batchReads = this._batchReads;
var batchWrites = this._batchWrites;
var length = queue.length;
var id;
var i;
// Reset ticker.
this._nextStep = null;
// Setup queues and callback placeholders.
for (i = 0; i < length; i++) {
id = queue[i];
if (!id) continue;
batch.push(id);
batchReads[id] = reads[id];
delete reads[id];
batchWrites[id] = writes[id];
delete writes[id];
}
// Reset queue.
queue.length = 0;
// Process read callbacks.
for (i = 0; i < length; i++) {
id = batch[i];
if (batchReads[id]) {
batchReads[id]();
delete batchReads[id];
}
}
// Process write callbacks.
for (i = 0; i < length; i++) {
id = batch[i];
if (batchWrites[id]) {
batchWrites[id]();
delete batchWrites[id];
}
}
// Reset batch.
batch.length = 0;
// Restart the ticker if needed.
if (!this._nextStep && queue.length) {
this._nextStep = raf(this._step);
}
};
var ticker = new Ticker();
var layoutTick = 'layout';
var visibilityTick = 'visibility';
var moveTick = 'move';
var scrollTick = 'scroll';
var placeholderTick = 'placeholder';
function addLayoutTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + layoutTick, readCallback, writeCallback);
}
function cancelLayoutTick(itemId) {
return ticker.cancel(itemId + layoutTick);
}
function addVisibilityTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + visibilityTick, readCallback, writeCallback);
}
function cancelVisibilityTick(itemId) {
return ticker.cancel(itemId + visibilityTick);
}
function addMoveTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + moveTick, readCallback, writeCallback, true);
}
function cancelMoveTick(itemId) {
return ticker.cancel(itemId + moveTick);
}
function addScrollTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + scrollTick, readCallback, writeCallback, true);
}
function cancelScrollTick(itemId) {
return ticker.cancel(itemId + scrollTick);
}
function addPlaceholderTick(itemId, readCallback, writeCallback) {
return ticker.add(itemId + placeholderTick, readCallback, writeCallback);
}
function cancelPlaceholderTick(itemId) {
return ticker.cancel(itemId + placeholderTick);
}
var ElProto = window.Element.prototype;
var matchesFn =
ElProto.matches ||
ElProto.matchesSelector ||
ElProto.webkitMatchesSelector ||
ElProto.mozMatchesSelector ||
ElProto.msMatchesSelector ||
ElProto.oMatchesSelector ||
function() {
return false;
};
/**
* Check if element matches a CSS selector.
*
* @param {Element} el
* @param {String} selector
* @returns {Boolean}
*/
function elementMatches(el, selector) {
return matchesFn.call(el, selector);
}
/**
* Add class to an element.
*
* @param {HTMLElement} element
* @param {String} className
*/
function addClass(element, className) {
if (element.classList) {
element.classList.add(className);
} else {
if (!elementMatches(element, '.' + className)) {
element.className += ' ' + className;
}
}
}
var tempArray = [];
var numberType = 'number';
/**
* Insert an item or an array of items to array to a specified index. Mutates
* the array. The index can be negative in which case the items will be added
* to the end of the array.
*
* @param {Array} array
* @param {*} items
* @param {Number} [index=-1]
*/
function arrayInsert(array, items, index) {
var startIndex = typeof index === numberType ? index : -1;
if (startIndex < 0) startIndex = array.length - startIndex + 1;
array.splice.apply(array, tempArray.concat(startIndex, 0, items));
tempArray.length = 0;
}
/**
* Normalize array index. Basically this function makes sure that the provided
* array index is within the bounds of the provided array and also transforms
* negative index to the matching positive index.
*
* @param {Array} array
* @param {Number} index
* @param {Boolean} isMigration
*/
function normalizeArrayIndex(array, index, isMigration) {
var length = array.length;
var maxIndex = Math.max(0, isMigration ? length : length - 1);
return index > maxIndex ? maxIndex : index < 0 ? Math.max(maxIndex + index + 1, 0) : index;
}
/**
* Move array item to another index.
*
* @param {Array} array
* @param {Number} fromIndex
* - Index (positive or negative) of the item that will be moved.
* @param {Number} toIndex
* - Index (positive or negative) where the item should be moved to.
*/
function arrayMove(array, fromIndex, toIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var from = normalizeArrayIndex(array, fromIndex);
var to = normalizeArrayIndex(array, toIndex);
// Add target item to the new position.
if (from !== to) {
array.splice(to, 0, array.splice(from, 1)[0]);
}
}
/**
* Swap array items.
*
* @param {Array} array
* @param {Number} index
* - Index (positive or negative) of the item that will be swapped.
* @param {Number} withIndex
* - Index (positive or negative) of the other item that will be swapped.
*/
function arraySwap(array, index, withIndex) {
// Make sure the array has two or more items.
if (array.length < 2) return;
// Normalize the indices.
var indexA = normalizeArrayIndex(array, index);
var indexB = normalizeArrayIndex(array, withIndex);
var temp;
// Swap the items.
if (indexA !== indexB) {
temp = array[indexA];
array[indexA] = array[indexB];
array[indexB] = temp;
}
}
var actionCancel = 'cancel';
var actionFinish = 'finish';
var debounceTick = 'debounce';
var debounceId = 0;
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds. The returned function accepts one argument which, when
* being "finish", calls the debounce function immediately if it is currently
* waiting to be called, and when being "cancel" cancels the currently queued
* function call.
*
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
function debounce(fn, wait) {
var timeout;
var tickerId = ++debounceId + debounceTick;
if (wait > 0) {
return function(action) {
if (timeout !== undefined) {
timeout = window.clearTimeout(timeout);
ticker.cancel(tickerId);
if (action === actionFinish) fn();
}
if (action !== actionCancel && action !== actionFinish) {
timeout = window.setTimeout(function() {
timeout = undefined;
ticker.add(tickerId, fn, null, true);
}, wait);
}
};
}
return function(action) {
if (action !== actionCancel) fn();
};
}
/**
* Returns true if element is transformed, false if not. In practice the
* element's display value must be anything else than "none" or "inline" as
* well as have a valid transform value applied in order to be counted as a
* transformed element.
*
* Borrowed from Mezr (v0.6.1):
* https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L661
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
function isTransformed(element) {
var transform = getStyle(element, 'transform');
if (!transform || transform === 'none') return false;
var display = getStyle(element, 'display');
if (display === 'inline' || display === 'none') return false;
return true;
}
/**
* Returns an absolute positioned element's containing block, which is
* considered to be the closest ancestor element that the target element's
* positioning is relative to. Disclaimer: this only works as intended for
* absolute positioned elements.
*
* @param {HTMLElement} element
* @param {Boolean} [includeSelf=false]
* - When this is set to true the containing block checking is started from
* the provided element. Otherwise the checking is started from the
* provided element's parent element.
* @returns {(Document|Element)}
*/
function getContainingBlock(element, includeSelf) {
// As long as the containing block is an element, static and not
// transformed, try to get the element's parent element and fallback to
// document. https://github.com/niklasramo/mezr/blob/0.6.1/mezr.js#L339
var document = window.document;
var ret = (includeSelf ? element : element.parentElement) || document;
while (ret && ret !== document && getStyle(ret, 'position') === 'static' && !isTransformed(ret)) {
ret = ret.parentElement || document;
}
return ret;
}
/**
* Returns the computed value of an element's style property transformed into
* a float value.
*
* @param {HTMLElement} el
* @param {String} style
* @returns {Number}
*/
function getStyleAsFloat(el, style) {
return parseFloat(getStyle(el, style)) || 0;
}
var offsetA = {};
var offsetB = {};
var offsetDiff = {};
/**
* Returns the element's document offset, which in practice means the vertical
* and horizontal distance between the element's northwest corner and the
* document's northwest corner. Note that this function always returns the same
* object so be sure to read the data from it instead using it as a reference.
*
* @param {(Document|Element|Window)} element
* @param {Object} [offsetData]
* - Optional data object where the offset data will be inserted to. If not
* provided a new object will be created for the return data.
* @returns {Object}
*/
function getOffset(element, offsetData) {
var ret = offsetData || {};
var rect;
// Set up return data.
ret.left = 0;
ret.top = 0;
// Document's offsets are always 0.
if (element === document) return ret;
// Add viewport scroll left/top to the respective offsets.
ret.left = window.pageXOffset || 0;
ret.top = window.pageYOffset || 0;
// Window's offsets are the viewport scroll left/top values.
if (element.self === window.self) return ret;
// Add element's client rects to the offsets.
rect = element.getBoundingClientRect();
ret.left += rect.left;
ret.top += rect.top;
// Exclude element's borders from the offset.
ret.left += getStyleAsFloat(element, 'border-left-width');
ret.top += getStyleAsFloat(element, 'border-top-width');
return ret;
}
/**
* Calculate the offset difference two elements.
*
* @param {HTMLElement} elemA
* @param {HTMLElement} elemB
* @param {Boolean} [compareContainingBlocks=false]
* - When this is set to true the containing blocks of the provided elements
* will be used for calculating the difference. Otherwise the provided
* elements will be compared directly.
* @returns {Object}
*/
function getOffsetDiff(elemA, elemB, compareContainingBlocks) {
offsetDiff.left = 0;
offsetDiff.top = 0;
// If elements are same let's return early.
if (elemA === elemB) return offsetDiff;
// Compare containing blocks if necessary.
if (compareContainingBlocks) {
elemA = getContainingBlock(elemA, true);
elemB = getContainingBlock(elemB, true);
// If containing blocks are identical, let's return early.
if (elemA === elemB) return offsetDiff;
}
// Finally, let's calculate the offset diff.
getOffset(elemA, offsetA);
getOffset(elemB, offsetB);
offsetDiff.left = offsetB.left - offsetA.left;
offsetDiff.top = offsetB.top - offsetA.top;
return offsetDiff;
}
var styleOverflow = 'overflow';
var styleOverflowX = 'overflow-x';
var styleOverflowY = 'overflow-y';
var overflowAuto = 'auto';
var overflowScroll = 'scroll';
/**
* Check if an element is scrollable.
*
* @param {HTMLElement} element
* @returns {Boolean}
*/
function isScrollable(element) {
var overflow = getStyle(element, styleOverflow);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
overflow = getStyle(element, styleOverflowX);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
overflow = getStyle(element, styleOverflowY);
if (overflow === overflowAuto || overflow === overflowScroll) return true;
return false;
}
/**
* Collect element's ancestors that are potentially scrollable elements.
*
* @param {HTMLElement} element
* @param {Boolean} [includeSelf=false]
* @param {Array} [data]
* @returns {Array}
*/
function getScrollableAncestors(element, includeSelf, data) {
var ret = data || [];
var parent = includeSelf ? element : element.parentNode;
// Find scroll parents.
while (parent && parent !== document) {
// If element is inside ShadowDOM let's get it's host node from the real
// DOM and continue looping.
if (parent.getRootNode && parent instanceof DocumentFragment) {
parent = parent.getRootNode().host;
continue;
}
// If element is scrollable let's add it to the scrollable list.
if (isScrollable(parent)) {
ret.push(parent);
}
parent = parent.parentNode;
}
// Always add window to the results.
ret.push(window);
return ret;
}
var translateValue = {};
var transformStyle$2 = 'transform';
var transformNone = 'none';
var rxMat3d = /^matrix3d/;
var rxMatTx = /([^,]*,){4}/;
var rxMat3dTx = /([^,]*,){12}/;
var rxNextItem = /[^,]*,/;
/**
* Returns the element's computed translateX and translateY values as a floats.
* The returned object is always the same object and updated every time this
* function is called.
*
* @param {HTMLElement} element
* @returns {Object}
*/
function getTranslate(element) {
translateValue.x = 0;
translateValue.y = 0;
var transform = getStyle(element, transformStyle$2);
if (!transform || transform === transformNone) {
return translateValue;
}
// Transform style can be in either matrix3d(...) or matrix(...).
var isMat3d = rxMat3d.test(transform);
var tX = transform.replace(isMat3d ? rxMat3dTx : rxMatTx, '');
var tY = tX.replace(rxNextItem, '');