Source: deliteful/SwapView.js

/** @module deliteful/SwapView */
define([
	"dcl/dcl", "delite/register",
	"requirejs-dplugins/jquery!attributes/classes",
	"dpointer/events", "./ViewStack",
	"delite/theme!./SwapView/themes/{{theme}}/SwapView.css"
], function (dcl, register, $, dpointer, ViewStack) {
	/**
	 * SwapView container widget. Extends ViewStack to let the user swap the visible child using a swipe gesture.
	 * You can also use the Page Up / Down keyboard keys to go to the next/previous child.
	 *
	 * @example
	 * <d-swap-view id="sv">
	 *     <div id="childA">...</div>
	 *     <div id="childB">...</div>
	 *     <div id="childC">...</div>
	 * </d-swap-view>
	 * @class module:deliteful/SwapView
	 * @augments module:deliteful/ViewStack
	 */
	return register("d-swap-view", [HTMLElement, ViewStack], /** @lends module:deliteful/SwapView# */{
		/**
		 * The name of the CSS class of this widget. Note that this element also use the d-view-stack class to
		 * leverage `deliteful/ViewStack` styles.
		 * @member {string}
		 * @default "d-swap-view"
		 */
		baseClass: "d-swap-view",

		/**
		 * Drag threshold: drag will start only if the user moves the pointer more than this threshold.
		 * @member {number}
		 * @default 10
		 * @private
		 */
		_dragThreshold: 10,

		/**
		 * Swap threshold: number between 0 and 1 that determines the minimum swipe gesture to swap the view.
		 * Default is 0.25 which means that you must drag (horizontally) by more than 1/4 of the view size to swap
		 * views.
		 * @member {number}
		 * @default 0.25
		 */
		swapThreshold: 0.25,

		createdCallback: function () {
			this.on("pointerdown", this._pointerDownHandler.bind(this));
			this.on("pointermove", this._pointerMoveHandler.bind(this));
			this.on("pointerup", this._pointerUpHandler.bind(this));
			this.on("lostpointercapture", this._pointerUpHandler.bind(this));
			this.on("pointercancel", this._pointerUpHandler.bind(this));
			this.on("keydown", this._keyDownHandler.bind(this));
		},

		render: function () {
			// we want to inherit from ViewStack's CSS (including transitions).
			$(this).addClass("d-view-stack");

			dpointer.setTouchAction(this, "pan-y");
		},

		attachedCallback: function () {
			// If the user hasn't specified a tabindex declaratively, then set to default value.
			if (!this.hasAttribute("tabindex")) {
				this.tabIndex = "0";
			}
		},

		/**
		 * Starts drag/swipe interaction.
		 * @private
		 */
		_pointerDownHandler: function (e) {
			if (!this._drag) {
				this._drag = { start: e.clientX };
				dpointer.setPointerCapture(e.target, e.pointerId);
			}
		},

		/**
		 * Handle pointer move events during drag/swipe interaction.
		 * @private
		 */
		_pointerMoveHandler: function (e) {
			/* jshint maxcomplexity: 13 */
			if (this._drag) {
				var dx = e.clientX - this._drag.start;
				if (!this._drag.started && Math.abs(dx) > this._dragThreshold) {
					// user dragged (more than the threshold), start sliding children.
					var childOut = this._visibleChild;
					var childIn = (this.effectiveDir === "ltr" ? dx < 0 : dx > 0) ? childOut.nextElementSibling :
						childOut.previousElementSibling;
					if (childIn) {
						this._drag.childOut = childOut;
						this._drag.childIn = childIn;
						this._drag.started = true;
						this._drag.ended = false;

						this._drag.reverse = dx > 0;

						$(this).addClass("-d-swap-view-drag");

						childIn.style.visibility = "visible";
						childIn.style.display = "";
					}
				}
				if (this._drag.started && !this._drag.ended) {
					// This is what will really translate the children as the user swipes/drags.
					var rx = this._drag.rx = dx / this.offsetWidth;

					var v = this._drag.reverse ? rx : -rx;

					var lv = Math.floor((this._drag.reverse ? 1 - v : v) * 100);
					var rv = Math.floor((this._drag.reverse ? v : 1 - v) * 100);

					var left = this._drag.reverse ? this._drag.childIn : this._drag.childOut;
					var right = this._drag.reverse ? this._drag.childOut : this._drag.childIn;

					this._setTranslation(left, -lv);
					this._setTranslation(right, rv);
				}
			}
		},

		/**
		 * Handle end of drag/swipe interaction.
		 * @private
		 */
		_pointerUpHandler: function () {
			if (this._drag) {
				if (!this._drag.started) {
					// abort before user really dragged
					this._drag = null;
				} else if (!this._drag.ended) {
					// user released finger/mouse
					this._drag.ended = true;

					this._setupTransitionEndHandlers();

					this._setTransitionProperties(this._drag.childIn);
					this._setTransitionProperties(this._drag.childOut);

					if ((this._drag.reverse && this._drag.rx > this.swapThreshold) ||
						(!this._drag.reverse && this._drag.rx < -this.swapThreshold)) {
						// user dragged more than the swap threshold: finish sliding to the next/prev child.
						this._setTranslation(this._drag.childIn, 0);
						this._setTranslation(this._drag.childOut, this._drag.reverse ? 100 : -100);
					} else {
						// user dragged less then the swap threshold: slide back to the current child.
						this._drag.slideBack = true;
						this._setTranslation(this._drag.childIn, this._drag.reverse ? -100 : 100);
						this._setTranslation(this._drag.childOut, 0);
					}
				}
			}
		},

		/**
		 * Handle Page Up/Down keys to show the previous/next child.
		 * @private
		 */
		_keyDownHandler: function (e) {
			switch (e.key) {
			case "PageUp":
				this.showNext();
				break;
			case "PageDown":
				this.showPrevious({reverse: true});
				break;
			}
		},

		_setupTransitionEndHandlers: function () {
			// set listeners to cleanup all CSS classes after the slide transition (either from ViewStack::show,
			// of from the slide back animation).
			if (!this._endTransitionHandler) {
				this._endTransitionHandler = function () {
					if (this._endTransitionHandler) {
						this._addTransitionEndHandlers(this._drag.childIn, false);
						this._addTransitionEndHandlers(this._drag.childOut, false);
						this._endTransitionHandler = null;
					}
					this._endTransition();
				}.bind(this);
				this._addTransitionEndHandlers(this._drag.childIn, true);
				this._addTransitionEndHandlers(this._drag.childOut, true);
			}
		},

		/**
		 * Cleanup all CSS classes and added rules after transition.
		 * @private
		 */
		_endTransition: function () {
			if (this._drag) {
				$(this).removeClass("-d-swap-view-drag");

				if (this._drag.slideBack) {
					// Hide the "in" view if the wap was cancelled (slide back).
					this._drag.childIn.style.visibility = "hidden";
					this._drag.childIn.style.display = "none";
				} else {
					this._drag.childOut.style.visibility = "hidden";
					this._drag.childOut.style.display = "none";
					this.show(this._drag.childIn, {transition: "none"});
				}

				this._clearTransitionProperties(this._drag.childIn);
				this._clearTransitionProperties(this._drag.childOut);

				this._clearTranslation(this._drag.childIn);
				this._clearTranslation(this._drag.childOut);

				this._drag = null;
			}
		},

		// CSS/events utilities

		_addTransitionEndHandlers: function (child, add) {
			var m = (add ? "add" : "remove") + "EventListener";
			child[m]("webkitTransitionEnd", this._endTransitionHandler);
			child[m]("transitionend", this._endTransitionHandler); // IE10 + FF
		},

		_setTransitionProperties: function (child) {
			child.style.webkitTransitionProperty = "-webkit-transform";
			child.style.transitionProperty = "transform";
			child.style.webkitTransitionDuration = "0.3s";
			child.style.mozTransitionDuration = "0.3s";
			child.style.transitionDuration = "0.3s";
		},

		_clearTransitionProperties: function (child) {
			child.style.webkitTransitionProperty = "";
			child.style.transitionProperty = "";
			child.style.webkitTransitionDuration = "";
			child.style.mozTransitionDuration = "";
			child.style.transitionDuration = "";
		},

		_setTranslation: function (child, percent) {
			var t = "translate3d(" + percent + "%, 0, 0)";
			child.style.webkitTransform = t;
			child.style.transform = t;
		},

		_clearTranslation: function (child) {
			child.style.webkitTransform = "";
			child.style.transform = "";
		}
	});
});