UNPKG

survey-analytics

Version:

SurveyJS analytics Library.

1,745 lines (1,495 loc) 597 kB
/*! * 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, '');