Source: deliteful/SidePane.js

/** @module deliteful/SidePane */
define([
	"dcl/dcl",
	"dpointer/events",
	"requirejs-dplugins/jquery!attributes/classes",
	"decor/sniff",
	"delite/register",
	"delite/DisplayContainer",
	"requirejs-dplugins/Promise!",
	"delite/theme!./SidePane/themes/{{theme}}/SidePane.css"
],
	function (dcl, pointer, $, has, register, DisplayContainer, Promise) {
		function prefix(v) {
			return "-d-side-pane-" + v;
		}
		function setVisibility(node, val) {
			if (val) {
				node.style.visibility = "visible";
				node.style.display = "block";
			} else {
				node.style.visibility = "hidden";
				node.style.display = "none";
			}
		}
		function getNextSibling(node) {
			do {
				node = node.nextElementSibling;
			} while (node && node.nodeType !== 1);
			return node;
		}
		/**
		 * A widget displayed on the side of the screen.
		 *
		 * It can be displayed on top of the page
		 * (mode=overlay) or can push the content of the page (mode=push or mode=reveal).
		 * SidePane is a widget hidden by default.
		 * This widget must be a sibling of html's body element.
		 * If mode is set to "push" or "reveal", the width of the SidePane can't be changed in the markup
		 * (15em by default).
		 * However it can be changed in SidePane.less (@PANE_WIDTH variable).
		 * In "push" and "reveal" mode, the pushed element is the first sibling of the SidePane which has
		 * of type element (nodeType == 1) and not a SidePane.
		 * @example
		 * <body>
		 *   <d-side-pane>
		 *       SidePane content
		 *   </d-side-pane>
		 *   <div>
		 *       Main application
		 *   </div>
		 * </body>
		 * @class module:deliteful/SidePane
		 * @augments module:delite/DisplayContainer
		 */
		return register("d-side-pane", [HTMLElement, DisplayContainer],
			/** @lends module:deliteful/SidePane#*/ {
			/**
			 * The name of the CSS class of this widget.
			 * @member {string}
			 * @default "d-side-pane"
			 */
			baseClass: "d-side-pane",

			/**
			 * Can be "overlay", "reveal" or "push".
			 * In overlay mode, the pane is shown on top of the page.
			 * In reveal and push modes, The page is moved to make the pane visible. The difference between
			 * these two modes is the animated transition: in reveal mode, the pane does not move, it is
			 * already under the page. In push mode, the pane slide with the page.
			 * @member {string}
			 * @default "push"
			 */
			mode: "push",

			/**
			 * Can be "start" or "end". If set to "start", the panel is displayed on the
			 * left side in LTR mode.
			 * @member {string}
			 * @default "push"
			 */
			position: "start",

			/**
			 * Enable/Disable animations.
			 * @member {boolean}
			 * @default true
			 */
			animate: true,

			/**
			 * Enables the swipe closing of the pane.
			 * @member {boolean}
			 * @default true
			 */
			swipeClosing: true,

			_transitionTiming: {default: 0, chrome: 50, ios: 20, android: 100, ff: 100},
			_timing: 0,
			_visible: false,
			_opening: false,
			_originX: NaN,
			_originY: NaN,

			attachedCallback: function () {
				this.parentNode.style.overflow = "hidden";
			},

			show: dcl.superCall(function (sup) {
				return function () {
					if (arguments.length > 0) {
						return sup.apply(this, arguments).then(function (value) {
							return this._open().then(function () {
								return value;
							});
						}.bind(this));
					} else {
						return this._open();
					}
				};
			}),

			hide: dcl.superCall(function (sup) {
				return function () {
					if (arguments.length > 0) {
						return sup.apply(this, arguments).then(function (value) {
							return this._close().then(function () {
								return value;
							});
						}.bind(this));
					} else {
						return this._close();
					}
				};
			}),
			/**
			 * This method is called to toggle the visibility of the SidePane.
			 * @returns {Promise} A promise that will be resolved when the display & transition effect will have been
			 * performed.
			 */
			toggle: function () {
				return this._visible ? this.hide() : this.show();
			},

			/**
			 * Open the pane.
			 * @private
			 */
			_open: function () {
				var promise;
				var nextElement = getNextSibling(this);
				var animate = this.animate && has("ie") !== 9;
				if (!this._visible) {
					if (animate) {
						$(this).addClass(prefix("animate"));
						if (nextElement) {
							$(nextElement).addClass(prefix("animate"));
						}
					}

					if (this.mode === "reveal") {
						if (nextElement) {
							promise = this._setAfterTransitionHandlers(nextElement);
						}
					} else {
						promise = this._setAfterTransitionHandlers(this);
					}

					setVisibility(this, true);

					if (animate) {
						this.defer(this._openImpl, this._timing);
					} else {
						this._openImpl();
						promise = new Promise(function (resolve) {
							this.defer(resolve, this._timing);
						}.bind(this));
					}
				}
				return promise || Promise.resolve(true);
			},

			/**
			 * Close the pane.
			 * @private
			 */
			_close: function () {
				var promise;
				if (this._visible) {
					if (this.mode === "reveal") {
						var nextElement = getNextSibling(this);
						if (nextElement) {
							promise = this._setAfterTransitionHandlers(nextElement);
						}
					} else {
						promise = this._setAfterTransitionHandlers(this);
					}

					if (this.animate && has("ie") !== 9) {
						// This defer should be useless but is needed for Firefox, see #25
						this.defer(function () {this._hideImpl(); }, this._timing);
					} else {
						this._hideImpl();
						setVisibility(this, false);
					}
				}
				return promise || Promise.resolve(true);
			},

			_setAfterTransitionHandlers: function (node) {
				var self = this, holder = { node: node};
				var promise = new Promise(function (resolve) {
					holder.handle =  function () { self._afterTransitionHandle(holder, resolve); };
				});
				node.addEventListener("webkitTransitionEnd", holder.handle);
				node.addEventListener("transitionend", holder.handle); // IE10 + FF
				return promise;
			},

			_afterTransitionHandle: function (holder, resolve) {
				$(this).removeClass(prefix("under"));
				if (!this._visible) {
					setVisibility(this, false);
				}
				holder.node.removeEventListener("webkitTransitionEnd", holder.handle);
				holder.node.removeEventListener("transitionend", holder.handle);
				resolve();
			},

			postRender: function () {
				setVisibility(this, false);
			},

			preRender: function () {
				this._transitionTiming = {default: 0, chrome: 20, ios: 20, android: 100, ff: 100};
				for (var o in this._transitionTiming) {
					if (has(o) && this._timing < this._transitionTiming[o]) {
						this._timing = this._transitionTiming[o];
					}
				}
			},

			render: function () {
				pointer.setTouchAction(this, "pan-y");
				this._resetInteractions();
			},

			_refreshMode: function (nextElement) {
				$(this).removeClass([prefix("push"), prefix("overlay"), prefix("reveal")].join(" "))
					.addClass(prefix(this.mode));

				if (nextElement && this._visible) {
					$(nextElement).toggleClass(prefix("translated"), this.mode !== "overlay");
				}

				if (this.mode === "reveal" && !this._visible) {
					// Needed by FF only for the first opening.
					$(this).removeClass(prefix("ontop"))
						.addClass(prefix("under"));
				}
				else if (this.mode === "overlay") {
					$(this).removeClass(prefix("under"))
						.addClass(prefix("ontop"));
				} else {
					$(this).removeClass([prefix("under"), prefix("ontop")].join(" "));
				}
			},

			_refreshPosition: function (nextElement) {
				$(this).removeClass([prefix("start"), prefix("end")].join(" "))
					.addClass(prefix(this.position));
				if (nextElement && this._visible) {
					$(nextElement).removeClass([prefix("start"), prefix("end")].join(" "))
						.addClass(prefix(this.position));
				}
			},

			refreshRendering: function (props) {
				if (!("mode" in props || "position" in props || "animate" in props)) {
					return;
				}
				var nextElement = getNextSibling(this);

				// Always remove animation during a refresh. Avoid to see the pane moving on mode changes.
				// Not very reliable on IE11.
				$(this).removeClass(prefix("animate"));

				if (nextElement) {
					$(nextElement).removeClass(prefix("animate"));
					$(nextElement).toggleClass("d-rtl", this.effectiveDir === "rtl");
				}

				if ("mode" in props) {
					this._refreshMode(nextElement);
				}

				if ("position" in props) {
					this._refreshPosition(nextElement);
				}

				$(this).toggleClass(prefix("hidden"), !this._visible)
					.toggleClass(prefix("visible"), this._visible);

				// Re-enable animation
				if (this.animate) {
					this.defer(function () {
						$(this).addClass(prefix("animate"));
						if (nextElement) {
							$(nextElement).addClass(prefix("animate"));
						}
					}, this._timing);
				}
			},

			_openImpl: function () {
				if (!this._visible) {
					this._visible = true;
					$(this).removeClass(prefix("hidden"))
						.addClass(prefix("visible"));

					if (this.mode === "push" || this.mode === "reveal") {
						var nextElement = getNextSibling(this);
						if (nextElement) {
							$(nextElement)
								.removeClass([prefix("nottranslated"), prefix("start"), prefix("end")].join(" "))
								.addClass([prefix(this.position), prefix("translated")].join(" "));
						}
					}
				}
			},

			_hideImpl: function () {
				if (this._visible) {
					this._visible = false;
					this._opening = false;
					$(this.ownerDocument.body).removeClass(prefix("no-select"));
					$(this).removeClass(prefix("visible"))
						.addClass(prefix("hidden"));
					if (this.mode === "push" || this.mode === "reveal") {
						var nextElement = getNextSibling(this);
						if (nextElement) {
							$(nextElement)
								.removeClass([prefix("translated"), prefix("start"), prefix("end")].join(" "))
								.addClass([prefix(this.position), prefix("nottranslated")].join(" "));
						}
					}
				}
			},

			_isLeft: function () {
				return (this.position === "start" && this.effectiveDir === "ltr") ||
					(this.position === "end" && this.effectiveDir === "rtl");
			},

			_pointerDownHandler: function (event) {
				this._originX = event.pageX;
				this._originY = event.pageY;

				if (this._visible || (this._isLeft() && !this._visible && this._originX <= 10) ||
					(!this._isLeft() && !this._visible && this._originX >= this.ownerDocument.width - 10)) {
					this._opening = !this._visible;
					this._pressHandle.remove();
					this._moveHandle = this.on("pointermove", this._pointerMoveHandler.bind(this));
					this._releaseHandle = this.on("pointerup", this._pointerUpHandler.bind(this));

					$(this.ownerDocument.body).addClass(prefix("no-select"));
				}
			},

			_pointerMoveHandler: function (event) {
				if (!this._opening && Math.abs(event.pageY - this._originY) > 10) {
					this._resetInteractions();
				} else {
					var pos = event.pageX;

					if (this._isLeft()) {
						if (this._visible) {
							if (this._originX < pos) {
								this._originX = pos;
							}

							if ((this.swipeClosing && this._originX - pos) > 10) {
								this._close();
								this._originX = pos;
							}
						}
					} else {
						if (this._visible) {
							if (this._originX > pos) {
								this._originX = pos;
							}
							if ((this.swipeClosing && pos - this._originX) > 10) {
								this._close();
								this._originX = pos;
							}
						}
					}
				}
			},

			_pointerUpHandler: function () {
				this._opening = false;
				$(this.ownerDocument.body).removeClass(prefix("no-select"));
				this._resetInteractions();
			},

			_resetInteractions: function () {
				if (this._releaseHandle) {
					this._releaseHandle.remove();
				}
				if (this._moveHandle) {
					this._moveHandle.remove();
				}
				if (this._pressHandle) {
					this._pressHandle.remove();
				}

				if (this.swipeClosing) {
					this._pressHandle = this.on("pointerdown", this._pointerDownHandler.bind(this));
				}

				this._originX = NaN;
				this._originY = NaN;
			}
		});
	});