Reusable Accessible Mapping Platform

API Docs for: 5.2.0
Show:

File: src\js\RAMP\Modules\stepItem.js

/* 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(),
                            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()
                            }, 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,
                        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;
    });