/* global define, $, TimelineLite */
/**
* @module RAMP
* @submodule FilterManager
* @main FilterManager
*/
/**
* Creates a step in the choice tree. A step item can contain several bricks in it and can take different states. Each step can advance and retreat by either displaying its selected child or hiding it.
*
* ####Imports RAMP Modules:
* {{#crossLink "Util"}}{{/crossLink}}
* {{#crossLink "TmplHelper"}}{{/crossLink}}
* {{#crossLink "TmplUtil"}}{{/crossLink}}
* {{#crossLink "Array"}}{{/crossLink}}
* {{#crossLink "Dictionary"}}{{/crossLink}}
* {{#crossLink "Bricks"}}{{/crossLink}}
*
*
* ####Uses RAMP Templates:
* {{#crossLink "templates/layer_selector_template.json"}}{{/crossLink}}
*
* @class StepItem
* @constructor
* @uses dojo/Evented
* @uses dojo/_base/declare
* @uses dojo/lang
* @uses dojo/Deferred
*
* @param {Object} config a config definition of the step item
* @param {String} config.id step item it; can be anything
* @param {Number} config.level level of this step item
* @param {Array} config.content an array of Brick configuration objects
* @param {String} config.content.[].id content brick id
* @param {Brick} config.content.[].type type of the content brick
* @param {Object} config.content.[].config a brick config object that will be passed to Brick.new() init function
* @param {Array} config.content.[].on a set of callbacks set on the create Brick object
* @param {String} config.content.[].on.[].eventName a name of the Brick event the callback should react to
* @param {Function} config.content.[].on.[].callback a function to be executed when the specified event is fired
*
* @return {StepItem} A built StepItem object.
*/
define([
"dojo/Evented", "dojo/_base/declare", "dojo/_base/lang", "dojo/Deferred",
/* Text */
"dojo/text!./templates/filter_manager_template.json",
/* Util */
"utils/util", "utils/tmplHelper", "utils/tmplUtil", "utils/array", "utils/dictionary", "utils/bricks"
],
function (
Evented, declare, lang, Deferred,
/* Text */
filter_manager_template,
/* Util */
UtilMisc, TmplHelper, TmplUtil, UtilArray, UtilDict, Bricks
) {
"use strict";
var StepItem,
ALL_STATES_CLASS,
templates = JSON.parse(TmplHelper.stringifyTemplate(filter_manager_template));
StepItem = declare([Evented], {
constructor: function (config) {
var that = this;
// declare individual properties inside the constructor: http://dojotoolkit.org/reference-guide/1.9/dojo/_base/declare.html#id6
lang.mixin(this,
{
/**
* Layer id. Upon initialization, `id` can be overwritten by `config.id` value.
*
* @property id
* @type String
* @default null
*/
id: null,
/**
* Indicates the level of the step, or how far down the tree this step appears.
*
* @property level
* @type Number
* @default 0
*
*/
level: 0,
/**
* A node of the StepItem.
*
* @property node
* @type JObject
* @default null
*/
node: null,
/**
* An array of Brick configs and other related properties.
*
* @property content
* @type {Array}
* @default null
* @private
* @example
* [{
* id: "sourceType",
* type: Bricks.ChoiceBrick,
* config: {
* header: i18n.t("addDataset.dataSource"),
* instructions: i18n.t("addDataset.help.dataSource"),
* choices: [
* {
* key: "serviceTypeStep",
* value: i18n.t("addDataset.dataSourceService")
* },
* {
* key: "fileTypeStep",
* value: i18n.t("addDataset.dataSourceFile")
* }
* ]
* },
* on: [
* {
* eventName: Bricks.ChoiceBrick.event.CHANGE,
* //expose: { as: "advance" },
* callback: choiceTreeCallbacks.simpleAdvance
* }
* ]
* }]
*
*/
content: null,
/**
* A collection of build Bricks that can accessed by their ids.
*
* @property contentBricks
* @type {Object}
* @default {}
*/
contentBricks: {},
/**
* Default template used for building step items.
*
* @property template
* @type {String}
* @default "default_step_template"
*/
template: "default_step_template",
/**
* Node of the content div.
*
* @private
* @property _contentNode
* @default null
*/
_contentNode: null,
/**
* Node of the options container.
*
* @private
* @property _optionsContainerNode
* @default null
*/
_optionsContainerNode: null,
/**
* Node of the options background node. It's used to change the state of the child steps - SUCCESS, ERROR, etc.
*
* @private
* @property _optionsBackgroundNode
* @default null
*/
_optionsBackgroundNode: null,
/**
* Node of the options div.
*
* @private
* @property _optionsNode
* @default null
*/
_optionsNode: null,
/**
* A collection of the child step items of this step item. Should not be accessed directly.
*
* @private
* @property _childSteps
* @default {}
*/
_childSteps: {},
/**
* A step item of the currently active child of this step item. If there is no active child, it means that a choice or action hasn't been made on this step yet or it's the last step in the branch.
*
* @private
* @property _activeChildStep
* @default null
*/
_activeChildStep: null,
/**
* A step item of the parent step item if any. Used only for animating background when opening/collapsing (error) notices.
*
* @private
* @property _parent
* @default null
*/
_parent: null,
/**
* An object containing some data. This is used like that: when the step is advanced, a data object is provided by external code; this object is then passed to whichever child is being advanced so it can be retrieved later without external code having to store it somewhere.
*
* @private
* @property _stepData
* @default null
*/
_stepData: {},
/**
* The current state of this step item.
*
* @private
* @property _state
* @default StepItem.state.DEFAULT,
*/
_state: StepItem.state.DEFAULT,
/**
* A timeline of this step item. Used for animation
*
* @private
* @property _timeline
*/
_timeline: new TimelineLite({ paused: true }),
/**
* A default duration value for all single transitions of any elements of this step.
*
* @private
* @property _transitionDuration
* @default 0.4
*/
_transitionDuration: 0.4
},
config
);
this.node = $(TmplHelper.template.call(null, this.template, config, templates));
this._contentNode = this.node.find("> .step-content");
this._optionsContainerNode = this.node.find("> .step-options-container");
this._optionsBackgroundNode = this._optionsContainerNode.find("> .options-bg");
this._optionsNode = this._optionsContainerNode.find("> .step-options");
this.content.forEach(function (contentItem) {
that._addContentBrick(contentItem);
});
// console.debug("-->", this._state);
},
/**
* Instantiates and adds a new brick to this step item.
*
* @method _addContentBrick
* @param {Object} contentItem a config object for a Brick
* @param {Object} contentItem.id Brick id
* @param {Object} contentItem.config actual Brick config
* @private
*/
_addContentBrick: function (contentItem) {
var that = this,
contentBrick = contentItem.type.new(contentItem.id, contentItem.config);
// if it's a multiBrick, add individual bricks from its content to the main content and wire them as separate bricks
if (Bricks.MultiBrick === contentItem.type) {
contentBrick.content.forEach(function (contentItem) {
that._wireBrickUp(contentItem, contentBrick.contentBricks[contentItem.id]);
});
} else {
that._wireBrickUp(contentItem, contentBrick);
}
this._contentNode.append(contentBrick.node);
this._doInternalCheck();
},
/**
* Wire up listeners on the given Brick.
*
* @method _wireBrickUp
* @param {Object} contentItem a config object for a Brick
* @param {Object} contentBrick an actual Brick instance
* @private
*/
_wireBrickUp: function (contentItem, contentBrick) {
var that = this;
this.contentBricks[contentBrick.id] = contentBrick;
// set brick events if specified
if (contentItem.on) {
contentItem.on.forEach(function (o) {
contentBrick.on(o.eventName, function (data) {
// if there is a callback specified, call it in the context of the brick
if (o.callback) {
o.callback.call(contentBrick, that, data);
}
// if event is exposed; emit it
if (o.expose) {
that._doInternalCheck();
that.emit(contentBrick.id + "/" + o.eventName, data);
if (o.expose.as) {
that.emit(o.expose.as, {
brick: contentBrick,
brickData: data
});
}
}
});
});
}
// do a check of all the bricks in case some of them depend on validity of other bricks in this step
contentBrick.on(Bricks.Brick.event.CHANGE, function () {
that._doInternalCheck();
});
},
/**
* Checks Brick's requirements. Enables or disabled the target Brick based on validity of its requirements.
*
* @method _internalCheckHelper
* @param {Array} required an array of required rules
* @param {Brick} targetBrick a Brick with requirements
* @param {Object} contentBricks a dictionary of bricks available in this step
* @private
*/
_internalCheckHelper: function (required, targetBrick, contentBricks) {
var flag = false;
switch (required.type) {
case "all":
flag = required.check.every(function (ch) {
return contentBricks[ch].isValid();
});
break;
case "any":
flag = required.check.some(function (ch) {
return contentBricks[ch].isValid();
});
break;
}
// disable or enable a brick based on sum validity of its dependencies
targetBrick.disable(!flag);
},
/**
* Checks this step item validity by checking validity of all its Bricks.
*
* @method _doInternalCheck
* @private
*/
_doInternalCheck: function () {
var that = this;
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
if (brick.required) {
// if it's a MultiBrick, check requirements for each of the Bricks in MultiBrick
if (Bricks.MultiBrick.isPrototypeOf(brick)) {
if (Array.isArray(brick.required)) {
brick.required.forEach(function (req) {
that._internalCheckHelper(req, brick.contentBricks[req.id], that.contentBricks);
});
} else {
that._internalCheckHelper(brick.required, brick, that.contentBricks);
}
} else {
that._internalCheckHelper(brick.required, brick, that.contentBricks);
}
}
});
// if the step in the error state and one of the Bricks is changed, switched to the DEFAULT state and switch all the content Bricks
if (this._state === StepItem.state.ERROR) {
this._notifyStateChange(StepItem.state.DEFAULT);
}
},
/**
* Creates timeline for retreat animation - when the part of the choice tree is collapsing, switching to another branch of the tree.
*
* @method _makeCloseTimeline
* @param {Boolean} skipFirst indicates whether the first child step should be included in the timeline
* @param {Boolean} resetState indicates if the child step state should be reset
* @return {Object} a constructed close timeline
* @private
*/
_makeCloseTimeline: function (skipFirst, resetState) {
var closeTimeline = new TimelineLite(),
closeStagger,
closeTimelines = [];
this._getCloseTimelines(closeTimelines, skipFirst, resetState);
closeTimelines = closeTimelines.reverse();
if (closeTimelines.length > 0) {
closeStagger = this._transitionDuration / 2 / closeTimelines.length;
closeTimeline.add(closeTimelines, "+=0", "start", closeStagger);
}
return closeTimeline;
},
/**
* Generates a close timeline for this particular step item and adds it to the global close timeline. Calls the same on the target child.
*
* @method _getCloseTimelines
* @param {Object} tls global close timeline
* @param {Boolean} skip indicates whether to skip the first child step item
* @param {Boolean} reset indicates whether to reset the step item state to DEFAULT
* @return {StepItem} itself
* @private
* @chainable
*/
_getCloseTimelines: function (tls, skip, reset) {
var tl = new TimelineLite(),
that = this;
if (this._activeChildStep) {
if (!skip) {
tl
.call(function () {
//that.currentLevel()
that._notifyCurrentStepChange();
})
.to(this._optionsContainerNode, this._transitionDuration,
{ top: -this._activeChildStep.getContentOuterHeight(), ease: "easeOutCirc" },
0)
.set(this._activeChildStep, { className: "-=active-option" })
.set(this._optionsContainerNode, { display: "none" })
;
tls.push(tl);
}
if (reset) {
this._notifyStateChange(StepItem.state.DEFAULT);
}
this._activeChildStep._getCloseTimelines(tls);
}
return this;
},
/**
* Creates timeline for shift animation - when the selected option for a choice is changing - animating horizontally.
*
* @method _makeShiftTimeline
* @param {String} targetChildStepId specifies the target childId
* @return {Object} a constructed shift timeline
* @private
*/
_makeShiftTimeline: function (targetChildStepId) {
var shiftTimeline = new TimelineLite(),
targetChildStep = this._childSteps[targetChildStepId],
allChildNodes = this._getChildNodes(),
otherChildNodes = this._getChildNodes([targetChildStepId]);
if (this._activeChildStep) {
shiftTimeline
.set(allChildNodes, { display: "inline-block" })
.to(this._optionsBackgroundNode, this._transitionDuration, {
height: targetChildStep.getContentOuterHeight(),
"line-height": targetChildStep.getContentOuterHeight() + 'px',
ease: "easeOutCirc"
}, 0)
.fromTo(this._optionsNode, this._transitionDuration,
{ left: -this._activeChildStep.getContentPosition().left },
{ left: -targetChildStep.getContentPosition().left, ease: "easeOutCirc" }, 0)
.set(otherChildNodes, { className: "-=active-option" }) // when shifting, active-option is changing
.set(targetChildStep.node, { className: "+=active-option" })
.set(this._optionsNode, { left: 0 })
.set(otherChildNodes, { display: "none" })
.call(function () {
targetChildStep._notifyStateChange(targetChildStep._state);
}, null, null, this._transitionDuration / 3)
;
}
return shiftTimeline;
},
/**
* Creates timeline for advance animation - when the part of the choice tree is unfolding, (after switching to another branch of the tree).
*
* @method _makeOpenTimeline
* @param {String} targetChildStepId specifies the target child id
* @param {Boolean} skipFirst indicates whether the first child step should be included in the timeline
* @return {Object} a constructed open timeline
* @private
*/
_makeOpenTimeline: function (targetChildStepId, skipFirst) {
var openTimeline = new TimelineLite(),
openStagger,
openTimelines = [];
this._getOpenTimelines(openTimelines, targetChildStepId, skipFirst);
if (openTimelines.length > 0) {
openStagger = this._transitionDuration / 2 / openTimelines.length;
openTimeline.add(openTimelines, "+=0", "start", openStagger);
}
return openTimeline;
},
/**
* Generates an open timeline for this particular step item and adds it to the global open timeline. Calls the same on the target child.
*
* @method _getOpenTimelines
* @param {Object} tls global open timeline
* @param {String} targetChildStepId specifies the target child id
* @param {Boolean} skip indicates whether to skip the first child step item
* @return {StepItem} itself
* @private
* @chainable
*/
_getOpenTimelines: function (tls, targetChildStepId, skip) {
var tl = new TimelineLite(),
targetChildStep = targetChildStepId ? this._childSteps[targetChildStepId] : this._activeChildStep,
otherChildNodes = this._getChildNodes([targetChildStepId]);
if (targetChildStep) {
if (!skip) {
tl
// set options container node to visible, otherwise you can't get its size
.set(this._optionsContainerNode, { display: "block", top: -9999 }, 0)
// make sure options' node is on the left
.set(this._optionsNode, { left: 0 }, 0)
// hide children other than target
.set(otherChildNodes, { display: "none" }, 0)
.set(targetChildStep.node, { className: "+=active-option", display: "inline-block" }, 0)
// make the target step current
.call(function () {
targetChildStep._notifyCurrentStepChange();
})
// animate step's background
.to(this._optionsBackgroundNode, 0, {
height: targetChildStep.getContentOuterHeight(),
"line-height": targetChildStep.getContentOuterHeight() + 'px'
}, 0)
// animate height and position of the options' container node
.to(this._optionsContainerNode, 0, { height: targetChildStep.getContentOuterHeight(), ease: "easeOutCirc" }, 0)
.fromTo(this._optionsContainerNode, this._transitionDuration,
{ top: -this._optionsContainerNode.height() },
{ top: 0, ease: "easeOutCirc" },
0)
.set(this._optionsContainerNode, { height: "auto" })
;
tls.push(tl);
}
this._notifyStateChange(StepItem.state.SUCCESS);
// hide all notices when making a step successful
this.displayBrickNotices();
targetChildStep._getOpenTimelines(tls);
}
return this;
},
/**
* Returns an array of child step nodes except for steps whose ids are passed in `except` param.
*
* @method _getChildNodes
* @private
* @param {Array} except an array of child step ids to not include in the result
* @return {Array} an array of child step nodes
*/
_getChildNodes: function (except) {
var childNodes = [];
UtilDict.forEachEntry(this._childSteps,
function (childId, childItem) {
if (!except || except.indexOf(childItem.id) === -1) {
childNodes.push(childItem.node);
}
}
);
return childNodes;
},
/**
* Emits a `CURRENT_STEP_CHANGE` event with a payload of id and level of the current step item.
* This notifies the trunk of the tree and this step is now a current step. The trunk in turn notifies
* every other step that they are not current steps.
*
* @method _notifyCurrentStepChange
* @private
*/
_notifyCurrentStepChange: function () {
this._emit(StepItem.event.CURRENT_STEP_CHANGE, { id: this.id, level: this.level });
},
/**
* Emits a `STATE_CHANGE` event with a payload of id, level and state of the current step item.
* Additionally sets state of all the content bricks to corresponding states.
*
* @method _notifyStateChange
* @private
* @chainable
* @param {String} state state to set the step item to
* @return {StepItem} itself
*/
_notifyStateChange: function (state) {
var brickState;
this._state = state;
switch (state) {
case StepItem.state.SUCCESS:
brickState = Bricks.Brick.state.SUCCESS;
break;
case StepItem.state.ERROR:
brickState = Bricks.Brick.state.ERROR;
break;
default:
brickState = Bricks.Brick.state.DEFAULT;
break;
}
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
brick.setState(brickState);
});
this._emit(StepItem.event.STATE_CHANGE, { id: this.id, level: this.level, state: this._state });
return this;
},
/**
* A helper function to emit a supplied event with payload.
*
* @private
* @chainable
* @param {String} event event name
* @param {Object} [payload] payload object
* @return {StepItem} itself
*/
_emit: function (event, payload) {
this.emit(event, payload);
return this;
},
/**
* Returns step data and data from all content bricks.
*
* @return {Object} step data and brick data
*/
getData: function () {
var data = {
stepData: this._stepData,
bricksData: {}
};
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
lang.mixin(data.bricksData, brick.getData(true));
});
return data;
},
/**
* Adds a given step item object as a child for this step item.
*
* @method addChild
* @chainable
* @param {StepItem} stepItem a stepItem object to be added as a child.
* @return {StepItem} itself
*/
addChild: function (stepItem) {
this._optionsNode.append(stepItem.node);
this._childSteps[stepItem.id] = stepItem;
stepItem._parent = this;
return this;
},
/**
* Clears this step by resetting its state to `DEFAULT`, clearing all content bricks, and hide all brick notices.
*
* @method
* @param {Array} brickIds [description]
* @return {StepItem} itself
* @chainable
*/
clearStep: function (brickIds) {
var bricks = []; // bricks from whose notices should be hidden
// clear this steps state
this._notifyStateChange(StepItem.state.DEFAULT);
if (Array.isArray(brickIds)) {
brickIds.forEach(function (brickId) {
this.contentBricks[brickId].clear();
bricks.push(this.contentBricks[brickId]);
});
} else {
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
brick.clear();
bricks.push(brick);
});
}
// hide all notices when clearing the step
this.displayBrickNotices();
return this;
},
/**
* Sets the step specified by the `level` and `stepId` to a specified state.
*
* @method setState
* @param {Number} level level of the step to set the state on
* @param {String} stepId id of the step to set the state on
* @param {String} state name of the state to set
*/
setState: function (level, stepId, state) {
var that = this;
// if this step is the first step in the tree and so is the current step, set state class on its main node
if (this.level === 1 && level === 1) {
this.node
.removeClass(ALL_STATES_CLASS)
.addClass(state);
} else {
// if not, go over the children and if one corresponds to the current step, set state class on the options (children) container
UtilDict.forEachEntry(this._childSteps,
function (childId, childStep) {
if (childId === stepId && childStep.level === level) {
that._optionsContainerNode
.removeClass(ALL_STATES_CLASS)
.addClass(state);
}
}
);
}
},
/**
* Makes the step specified by the `level` and `stepId` a current step by setting a proper CSS class.
*
* @method currentStep
* @param {Number} level step level
* @param {String} stepId step id
*/
currentStep: function (level, stepId) {
var that = this;
// if this step is the first step in the tree and so is the current step, set class on the main node of this step
if (this.level === 1 && level === 1) {
this.node.addClass(StepItem.currentStepClass);
} else {
this.node.removeClass(StepItem.currentStepClass);
this._optionsContainerNode.removeClass(StepItem.currentStepClass);
// if not, go over the children and if one corresponds to the current step, set class on the options (children) container
UtilDict.forEachEntry(this._childSteps,
function (childId, childStep) {
if (childId === stepId && childStep.level === level) {
that._optionsContainerNode.addClass(StepItem.currentStepClass);
}
}
);
}
},
/**
* Checks if the step is valid. It's considered valid if all its content bricks are valid.
*
* @method isValid
* @return {Boolean} true if completed; false, otherwise
*/
isValid: function () {
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
if (!brick.isValid()) {
return false;
}
});
return true;
},
/**
* Checks if the step is completed. It's considered completed if its state is SUCCESS.
*
* @method isCompleted
* @return {Boolean} true if completed; false, otherwise
*/
isCompleted: function () {
return this._state === StepItem.state.SUCCESS;
},
/**
* Sets data to the content brick
*
* @method setData
* @param {Object} data a data object
* @param {Object} [data.bricksData] dictionary of data where keys are brick ids and values data to be passed to the corresponding bricks
* @param {Object} [data.stepData] some data object to be saved in this step
* @return {StepItem} itself
* @chainable
*/
setData: function (data) {
var that = this;
if (data) {
if (data.bricksData) {
UtilDict.forEachEntry(data.bricksData, function (brickId, brickData) {
that.contentBricks[brickId].setData(brickData);
});
}
if (data.stepData) {
this._stepData = data.stepData;
}
}
},
/**
* Set Brick notices, mostly errors.
*
* @method displayBrickNotices
* @param {Object} [data] a dictionary of objects containing Brick notices
*/
displayBrickNotices: function (data) {
var that = this,
bricks = [],
promise;
if (data) {
UtilDict.forEachEntry(data, function (brickId, brickData) {
that.contentBricks[brickId].displayNotice(brickData);
bricks.push(that.contentBricks[brickId]);
});
// toggle notice
this._toggleBrickNotices(bricks, data);
} else {
UtilDict.forEachEntry(this.contentBricks, function (key, brick) {
bricks.push(brick);
});
// if no data provided, first hide all existing notices, then empty them
promise = this._toggleBrickNotices(bricks, data);
promise.then(function () {
bricks.forEach(function (brick) {
brick.displayNotice();
});
});
}
},
/**
* Toggles the visibility of notices for specified bricks.
*
* @method _toggleBrickNotices
* @private
* @param {Array} bricks an array of Brick items to toggle notices on
* @param {Boolean} show a flag indicating whether to show or hide the notices
* @return {Promise} a promise that is resolved after animation is completed
*/
_toggleBrickNotices: function (bricks, show) {
var that = this,
notices,
contentHeight = this.getContentOuterHeight(),
heightChange = 0,
tl = new TimelineLite({ paused: true }),
def = new Deferred();
tl.eventCallback("onComplete", function () {
def.resolve();
});
// filter out bricks that don't have any notices
notices = bricks
.map(function (brick) { return brick.noticeNode; })
.filter(function (notice) { return notice.length > 0; })
;
if (show) {
tl.set(notices, { height: 0, visibility: "visible", position: "relative" }, 0);
}
// add notice animation to the timeline
notices.forEach(function (notice) {
heightChange += notice.height();
tl
.to(notice, that._transitionDuration / 2, { height: show ? notice.height() : 0, ease: "easeOutCirc" }, 0)
;
});
if (!show) {
tl.set(notices, { clearProps: "all" });
}
heightChange = show ? 0 : -heightChange;
// change the height of the parent's option background container to accommodate for notice height
if (this._parent) {
tl.to(this._parent._optionsBackgroundNode, this._transitionDuration / 2, {
height: contentHeight + heightChange,
'line-height': contentHeight + heightChange + 'px',
ease: "easeOutCirc"
}, 0);
}
tl.play();
return def.promise;
},
/**
* Retreats the current step item by collapsing its active children and resetting their states to default. After, active child step is set to null.
*
* @method retreat
* @return {StepItem} itself
* @chainable
*/
retreat: function () {
var closeTimeline,
that = this;
this._timeline
.seek("+=0", false)
.clear()
;
closeTimeline = this._makeCloseTimeline(false, true);
this._timeline
.add(closeTimeline)
.call(function () {
that._activeChildStep = null;
})
;
this._timeline.play(0);
return this;
},
/**
* Advances the current step to the step with the provided id. The target id has to be a child step.
* Additionally, the tree expands down if the target child has an active child as well, and so on, until no active child is present.
*
* @method advance
* @param {String} targetChildStepId id of the new target step of advance too
* @param {Object} [targetChildData] data to be passed to the target step as it opens
* @return {StepItem} itself
*/
advance: function (targetChildStepId, targetChildData) {
var closeTimeline,
shiftTimeline,
openTimeline,
targetChildStep = this._childSteps[targetChildStepId],
skipFirst,
that = this;
// cannot advance if the target is not specified
if (!targetChildStep) {
return this;
}
// reset timeline to the start and clear all the other rubbish that might be running already
this._timeline
.seek("+=0", false)
.clear()
;
// if there is already an active child step, skip the first animation
skipFirst = this._activeChildStep ? true : false;
targetChildStep.setData(targetChildData);
closeTimeline = this._makeCloseTimeline(skipFirst);
shiftTimeline = this._makeShiftTimeline(targetChildStepId);
openTimeline = this._makeOpenTimeline(targetChildStepId, skipFirst);
this._timeline
.add(closeTimeline)
.add(shiftTimeline)
.add(openTimeline)
.call(function () {
// only when animation completes, set the active child to the target child
that._activeChildStep = targetChildStep;
})
;
this._timeline.play(0);
return this;
},
/**
* Get position of the content node.
*
* @method getContentPosition
* @return {Object} jQuery position object of the content node
*/
getContentPosition: function () {
return this._contentNode.position();
},
/**
* Get outer height of the content node
*
* @method getContentOuterHeight
* @return {Number} outer height of the content node
*/
getContentOuterHeight: function () {
return this._contentNode.outerHeight();
}
});
lang.mixin(StepItem,
{
/**
* Specifies the current step CSS class name.
*
* @property currentStepClass
* @static
* @type {String}
*/
currentStepClass: "current-step",
/**
* A collection of possible StepItem states and their names.
*
* @propery state
* @static
* @type {Object}
* @example
* state: {
* SUCCESS: "step-state-success",
* ERROR: "step-state-error",
* DEFAULT: "step-state-default",
* LOADING: "step-state-loading"
* }
*
*/
state: {
SUCCESS: "step-state-success",
ERROR: "step-state-error",
DEFAULT: "step-state-default",
LOADING: "step-state-loading"
},
/**
* Event names published by the StepItem
*
* @property event
* @static
* @type Object
* @example
* {
* CURRENT_STEP_CHANGE: "stepItem/currentStepChange",
* STATE_CHANGE: "stepItem/stateChange"
* }
*/
event: {
/**
* Published whenever a StepItem becomes a current step. A current step has a distinct visual style.
*
* @event StepItem.event.CURRENT_STEP_CHANGE
* @param event {Object}
* @param event.level {Number} Level of the StepItem that became a current step
* @param event.id {String} Id of the StepItem that became a current step
*/
CURRENT_STEP_CHANGE: "stepItem/currentStepChange",
/**
* Published whenever a StepItem changes its state.
*
* @event StepItem.event.CURRENT_STEP_CHANGE
* @param event {Object}
* @param event.level {Number} Level of the StepItem that became a current step
* @param event.id {String} Id of the StepItem that became a current step
* @param event.state {String} name of the state
*/
STATE_CHANGE: "stepItem/stateChange"
}
}
);
// a string with all possible StepItem state CSS classes joined by " "; used to clear any CSS state class from the node
ALL_STATES_CLASS =
Object
.getOwnPropertyNames(StepItem.state)
.map(function (key) { return StepItem.state[key]; })
.join(" ");
return StepItem;
});