UNPKG

survey-analytics

Version:

SurveyJS Dashboard is a UI component for visualizing and analyzing survey data. It interprets the form JSON schema to identify question types and renders collected responses using interactive charts and tables.

1,752 lines (1,501 loc) 661 kB
/*! * surveyjs - SurveyJS Dashboard library v2.4.0 * Copyright (c) 2015-2025 Devsoft Baltic OÜ - http://surveyjs.io/ * License: SEE LICENSE IN LICENSE */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("survey-core")); else if(typeof define === 'function' && define.amd) define("SurveyAnalyticsCore", ["survey-core"], factory); else if(typeof exports === 'object') exports["SurveyAnalyticsCore"] = factory(require("survey-core")); else root["SurveyAnalyticsCore"] = factory(root["Survey"]); })(this, (__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, ''); translateValue.x = parseFloat(tX) || 0; translateValue.y = parseFloat(tY) || 0; return translateValue; } /** * Transform translateX and translateY va