/** @module deliteful/ViewStack */
define([
"dcl/dcl",
"decor/sniff",
"requirejs-dplugins/Promise!",
"requirejs-dplugins/jquery!attributes/classes",
"delite/register",
"delite/DisplayContainer",
"delite/theme!./ViewStack/themes/{{theme}}/ViewStack.css",
"requirejs-dplugins/css!./ViewStack/transitions/slide.css",
"requirejs-dplugins/css!./ViewStack/transitions/reveal.css"
], function (dcl, has, Promise, $, register, DisplayContainer) {
function setVisibility(node, val) {
if (node) {
if (val) {
node.style.visibility = "visible";
node.style.display = "";
} else {
node.style.visibility = "hidden";
node.style.display = "none";
}
}
}
function setReverse(node) {
if (node) {
$(node).addClass("-d-view-stack-reverse");
}
}
function cleanCSS(node) {
if (node) {
node.className = node.className.split(/ +/).filter(function (x) {
return !/^-d-view-stack/.test(x);
}).join(" ");
}
}
function transitionClass(s) {
return "-d-view-stack-" + s;
}
/**
* ViewStack container widget. Display one child at a time.
*
* The first child is displayed by default.
* The methods 'show' is used to change the visible child.
*
* Styling
* The following CSS attributes must not be changed.
* ViewStack node: position, box-sizing, overflow-x
* ViewStack children: position, box-sizing, width, height
*
* @example
* <d-view-stack id="vs">
* <div id="childA">...</div>
* <div id="childB">...</div>
* <div id="childC">...</div>
* </d-view-stack>
* <button onclick="vs.show(childB, {transition: 'reveal', reverse: true})">...</button>
* @class module:deliteful/ViewStack
* @augments module:delite/DisplayContainer
*/
return register("d-view-stack", [HTMLElement, DisplayContainer], /** @lends module:deliteful/ViewStack# */{
/**
* The name of the CSS class of this widget.
* @member {string}
* @default "d-view-stack"
*/
baseClass: "d-view-stack",
/**
* The transition type used if not specified in the second argument of the show method.
* Transitions type are: "none", "slide", "reveal", "flip", "fade".
* @member {string}
* @default "slide"
*/
transition: "slide",
/**
* If true, the transition animation is reversed.
* This attribute is supported by "slide" and "reveal" transition types.
* @member {boolean}
* @default false
*/
reverse: false,
/**
* The selected child id, can be set explicitly or through the show() method.
* The effect of setting this property (i.e. getting the value through the getter) might be
* asynchronous when an animated transition occurs.
* @member {string}
* @default ""
*/
selectedChildId: "",
_pendingChild: null,
_setSelectedChildIdAttr: function (child) {
if (this.ownerDocument.getElementById(child)) {
if (this.attached) {
this.show(child);
} else {
this._pendingChild = child;
}
}
},
_getSelectedChildIdAttr: function () {
return this._visibleChild ? this._visibleChild.id : "";
},
createdCallback: function () {
this._transitionTiming = {default: 0, chrome: 20, ios: 20, android: 100, ff: 100, ie: 20};
for (var o in this._transitionTiming) {
if (has(o) && this._timing < this._transitionTiming[o]) {
this._timing = this._transitionTiming[o];
}
}
},
attachedCallback: function () {
var noTransition = {transition: "none"};
if (this._pendingChild) {
this.show(this._pendingChild, noTransition);
this._pendingChild = null;
} else if (this.children.length > 0) {
this.show(this.children[0], noTransition);
}
},
_timing: 0,
_setChildrenVisibility: function () {
var cdn = this.children;
if (!this._visibleChild && cdn.length > 0) {
this._visibleChild = cdn[0];
}
for (var i = 0; i < cdn.length; i++) {
setVisibility(cdn[i], cdn[i] === this._visibleChild);
}
},
/*
* @private
*/
onAddChild: dcl.superCall(function (sup) {
return function (node) {
var res = sup.call(this, node);
this._setChildrenVisibility();
return res;
};
}),
postRender: function () {
this._setChildrenVisibility();
},
/**
* Shows the immediately following sibling of the ViewStack visible element.
* The parameter 'params' is optional. If not specified, this.transition, and this.reverse are used.
* @param {Object} [params] - Optional params. A hash like {transition: "reveal", reverse: true}.
* The transition value can be "slide", "overlay", "fade" or "flip". Reverse transition applies to "slide"
* and "reveal". Transition is internally set to "none" if the ViewStack is not visible.
* @returns {Promise} A promise that will be resolved when the display and transition effect will have
* been performed.
*/
showNext: function (params) {
// Shows the next child in the container.
return this._showPreviousNext("nextElementSibling", params);
},
/**
* Shows the immediately preceding sibling of the ViewStack visible element.
* The parameter 'params' is optional. If not specified, this.transition, and reverse = true are used.
* @param {Object} [params] - Optional params. A hash like {transition: "reveal", reverse: true}.
* The transition value can be "slide", "overlay", "fade" or "flip". Reverse transition applies to "slide"
* and "reveal". Transition is internally set to "none" if the ViewStack is not visible.
* Reverse is set to true if not specified.
* @returns {Promise} A promise that will be resolved when the display and transition effect will have
* been performed.
*/
showPrevious: function (params) {
// Shows the previous child in the container.
var args = {reverse: true};
dcl.mix(args, params || {});
return this._showPreviousNext("previousElementSibling", args);
},
_showPreviousNext: function (direction, props) {
var ret = null;
if (!this._visibleChild && this.children.length > 0) {
this._visibleChild = this.children[0];
}
if (this._visibleChild) {
var target = this._visibleChild[direction];
if (target) {
ret = this.show(target, props);
}
}
return ret;
},
_doTransition: function (origin, target, event, transition, reverse) {
var promises = [];
if (transition !== "none") {
if (origin) {
promises.push(this._setAfterTransitionHandlers(origin));
$(origin).addClass(transitionClass(transition));
}
if (target) {
promises.push(this._setAfterTransitionHandlers(target));
$(target).addClass(transitionClass(transition) + " -d-view-stack-in");
}
if (reverse) {
setReverse(origin);
setReverse(target);
}
this.defer(function () {
if (target) {
$(target).addClass("-d-view-stack-transition");
}
if (origin) {
$(origin).addClass("-d-view-stack-transition -d-view-stack-out");
}
if (reverse) {
setReverse(origin);
setReverse(target);
}
if (target) {
$(target).addClass("-d-view-stack-in");
}
}, this._timing);
} else {
if (origin !== target) {
setVisibility(origin, false);
}
}
return Promise.all(promises);
},
changeDisplay: function (widget, event) {
// Resolved when display is completed.
if (!widget || widget.parentNode !== this) {
return Promise.resolve();
}
var origin = this._visibleChild;
// Needed because the CSS state of a node can be incorrect
// if a previous transitionend has been dropped
cleanCSS(origin);
cleanCSS(widget);
setVisibility(widget, true);
this._visibleChild = widget;
var transition = (origin === widget) ? "none" : (event.transition || this.transition);
var reverse = this.effectiveDir === "ltr" ? event.reverse : !event.reverse;
return this._doTransition(origin, widget, event, transition, reverse);
},
/**
* Shows a children of the ViewStack. The parameter 'params' is optional. If not specified,
* this.transition, and this.reverse are used.
* This method must be called to display a particular destination child on this container.
* @param {Element|string} dest - Element or Element id that points to the child this container must
* show or hide.
* @param {Object} [params] - A hash like {transition: "reveal", reverse: true}. The transition value
* can be "slide", "overlay", "fade" or "flip". Reverse transition applies to "slide" and
* "reveal". Transition is internally set to "none" if the ViewStack is not visible.
* @returns {Promise} A promise that will be resolved when the display and transition effect will have
* been performed.
*/
show: dcl.superCall(function (sup) {
return function (dest, params) {
// Check visibility of the ViewStack, forces transition:"none" if not visible.
// - Transitions events are broken if the ViewStack is not visible
var parent = this;
while (parent && parent.style.display !== "none" && parent !== this.ownerDocument.body) {
parent = parent.parentNode;
}
if ((has("ie") === 9) || parent !== this.ownerDocument.body) {
if (! params) {
params = {};
}
params.transition = "none";
}
if (this._visibleChild && this._visibleChild.parentNode !== this) {
// The visible child has been removed.
this._visibleChild = null;
}
if (!this._visibleChild && this.children.length > 0) {
// The default visible child is the first one.
this._visibleChild = this.children[0];
}
return sup.apply(this, [dest, params]);
};
}),
_setAfterTransitionHandlers: function (node) {
var self = this, holder = { node: node};
holder.promise = new Promise(function (resolve) {
holder.handle = function () { self._afterTransitionHandle(holder, resolve); };
});
$(this).addClass("-d-view-stack-transition");
node.addEventListener("webkitTransitionEnd", holder.handle);
node.addEventListener("transitionend", holder.handle); // IE10 + FF
return holder.promise;
},
_afterTransitionHandle: function (holder, resolve) {
var isVisibleChild = this._visibleChild === holder.node;
setVisibility(holder.node, isVisibleChild);
cleanCSS(holder.node);
if (isVisibleChild) {
$(this).removeClass("-d-view-stack-transition");
}
holder.node.removeEventListener("webkitTransitionEnd", holder.handle);
holder.node.removeEventListener("transitionend", holder.handle);
resolve();
}
});
});