/** @module deliteful/SidePane */
define([
"dcl/dcl",
"dpointer/events",
"dojo/dom-class",
"decor/sniff",
"delite/register",
"delite/DisplayContainer",
"dojo/Deferred",
"delite/theme!./SidePane/themes/{{theme}}/SidePane.css",
"requirejs-dplugins/has!bidi?delite/theme!./SidePane/themes/{{theme}}/SidePane_rtl.css"
],
function (dcl, pointer, domClass, has, register, DisplayContainer, Deferred) {
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, mozilla: 100},
_timing: 0,
_visible: false,
_opening: false,
_originX: NaN,
_originY: NaN,
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 deferred = new Deferred();
var nextElement = getNextSibling(this);
if (!this._visible) {
if (this.animate) {
domClass.add(this, prefix("animate"));
if (nextElement) {
domClass.add(nextElement, prefix("animate"));
}
}
if (this.mode === "reveal") {
if (nextElement) {
this._setAfterTransitionHandlers(nextElement, {node: nextElement}, deferred);
}
} else {
this._setAfterTransitionHandlers(this, {node: this}, deferred);
}
setVisibility(this, true);
if (this.animate) {
this.defer(this._openImpl, this._timing);
} else {
this._openImpl();
this.defer(function () {deferred.resolve(); }, this._timing);
}
} else {
deferred.resolve();
}
return deferred.promise;
},
/**
* Close the pane.
* @private
*/
_close: function () {
var deferred = new Deferred();
if (this._visible) {
if (this.mode === "reveal") {
var nextElement = getNextSibling(this);
if (nextElement) {
this._setAfterTransitionHandlers(nextElement, {node: nextElement}, deferred);
}
} else {
this._setAfterTransitionHandlers(this, {node: this}, deferred);
}
if (this.animate) {
// 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);
}
} else {
deferred.resolve();
}
return deferred.promise;
},
_setAfterTransitionHandlers: function (node, event, deferred) {
var self = this, endProps = {
node: node,
handle: function () { self._afterTransitionHandle(endProps); },
props: event,
deferred: deferred
};
node.addEventListener("webkitTransitionEnd", endProps.handle);
node.addEventListener("transitionend", endProps.handle); // IE10 + FF
},
_afterTransitionHandle: function (item) {
domClass.remove(this, prefix("under"));
if (!this._visible) {
setVisibility(this, false);
}
item.node.removeEventListener("webkitTransitionEnd", item.handle);
item.node.removeEventListener("transitionend", item.handle);
item.deferred.resolve();
},
postCreate: function () {
setVisibility(this, false);
// trigger refreshRendering() to run and apply mode & position even if they are the default values
this.notifyCurrentValue("mode");
this.notifyCurrentValue("position");
},
preCreate: function () {
this._transitionTiming = {default: 0, chrome: 20, ios: 20, android: 100, mozilla: 100};
for (var o in this._transitionTiming) {
if (has(o) && this._timing < this._transitionTiming[o]) {
this._timing = this._transitionTiming[o];
}
}
},
buildRendering: function () {
pointer.setTouchAction(this, "none");
this._resetInteractions();
},
_refreshMode: function (nextElement) {
domClass.remove(this, [prefix("push"), prefix("overlay"), prefix("reveal")]);
domClass.add(this, prefix(this.mode));
if (nextElement && this._visible) {
domClass.toggle(nextElement, prefix("translated"), this.mode !== "overlay");
}
if (this.mode === "reveal" && !this._visible) {
// Needed by FF only for the first opening.
domClass.remove(this, prefix("ontop"));
domClass.add(this, prefix("under"));
}
else if (this.mode === "overlay") {
domClass.remove(this, prefix("under"));
domClass.add(this, prefix("ontop"));
} else {
domClass.remove(this, [prefix("under"), prefix("ontop")]);
}
},
_refreshPosition: function (nextElement) {
domClass.remove(this, [prefix("start"), prefix("end")]);
domClass.add(this, prefix(this.position));
if (nextElement && this._visible) {
domClass.remove(nextElement, [prefix("start"), prefix("end")]);
domClass.add(nextElement, prefix(this.position));
}
},
refreshRendering: function (props) {
this.parentNode.style.overflow = "hidden";
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.
domClass.remove(this, prefix("animate"));
if (nextElement) {
domClass.remove(nextElement, prefix("animate"));
if (!this.isLeftToRight()) {
domClass.add(nextElement, "d-rtl");
}
else {
domClass.remove(nextElement, "d-rtl");
}
}
if ("mode" in props) {
this._refreshMode(nextElement);
}
if ("position" in props) {
this._refreshPosition(nextElement);
}
domClass.toggle(this, prefix("hidden"), !this._visible);
domClass.toggle(this, prefix("visible"), this._visible);
// Re-enable animation
if (this.animate) {
this.defer(function () {
domClass.add(this, prefix("animate"));
if (nextElement) {
domClass.add(nextElement, prefix("animate"));
}
}, this._timing);
}
},
_openImpl: function () {
if (!this._visible) {
this._visible = true;
domClass.remove(this, prefix("hidden"));
domClass.add(this, prefix("visible"));
if (this.mode === "push" || this.mode === "reveal") {
var nextElement = getNextSibling(this);
if (nextElement) {
domClass.remove(nextElement, [prefix("nottranslated"), prefix("start"), prefix("end")]);
domClass.add(nextElement, [prefix(this.position), prefix("translated")]);
}
}
}
},
_hideImpl: function () {
if (this._visible) {
this._visible = false;
this._opening = false;
domClass.remove(this.ownerDocument.body, prefix("no-select"));
domClass.remove(this, prefix("visible"));
domClass.add(this, prefix("hidden"));
if (this.mode === "push" || this.mode === "reveal") {
var nextElement = getNextSibling(this);
if (nextElement) {
domClass.remove(nextElement, [prefix("translated"), prefix("start"), prefix("end")]);
domClass.add(nextElement, [prefix(this.position), prefix("nottranslated")]);
}
}
}
},
_isLeft: function () {
return (this.position === "start" && this.isLeftToRight()) ||
(this.position === "end" && !this.isLeftToRight());
},
_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));
domClass.add(this.ownerDocument.body, 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;
domClass.remove(this.ownerDocument.body, 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;
}
});
});