Skip to content

Instantly share code, notes, and snippets.

@ericf
Created December 20, 2010 20:42
Show Gist options
  • Select an option

  • Save ericf/748950 to your computer and use it in GitHub Desktop.

Select an option

Save ericf/748950 to your computer and use it in GitHub Desktop.
/**
* Guide
*
* Provides an interactive Guide Widget (Overlay) to present content in a series of Steps.
*
* GuideSteps are Widgets with WidgetStdMod support and are children to a Guide.
* These Steps also have WidgetPositionAlign support which is overloaded to reference related content for the Step;
* meaning a GuideStep can be aligned with it’s related content on the page, and when active,
* the Guide Overlay will position itself according the currently-selected Step’s defined alignment.
*
* Beyond providing containment for rendering GuideSteps, a Guide will only show the content of one Step at a time.
* A Guide provides navigation between it’s Steps both as a navigation list rendered in the Overlay
* and a ‘next’ button to select the next GuideStep child in the list.
*/
var Guide,
GUIDE = 'guide',
GuideStep,
GUIDE_STEP = 'guideStep',
CIRCULAR = 'circular',
STEPS_NODE = 'stepsNode',
NEXT_NODE = 'nextNode',
CLOSE_NODE = 'closeNode',
NAVIGATION = 'navigation',
NAVIGATION_NODE = 'navigationNode',
BOUNDING_BOX = 'boundingBox',
STD_HEADER = Y.WidgetStdMod.HEADER,
STD_BODY = Y.WidgetStdMod.BODY,
STD_FOOTER = Y.WidgetStdMod.FOOTER,
getCN = Y.ClassNameManager.getClassName,
DOT = '.',
STD_SECTION_CLASS_NAMES = Y.WidgetStdMod.SECTION_CLASS_NAMES,
NEXT = 'next',
CLOSE = 'close',
STEP = 'step',
STEPS = 'steps',
SELECTED = 'selected',
YLang = Y.Lang,
isString = YLang.isString,
isBoolean = YLang.isBoolean;
// *** GuideStep *** //
GuideStep = Y.Base.create(GUIDE_STEP, Y.Widget, [Y.WidgetStdMod, Y.WidgetPosition, Y.WidgetPositionAlign, Y.WidgetChild], {
// *** Prototype *** //
BOUNDING_TEMPLATE : '<li><li>',
CONTENT_TEMPLATE : null,
// *** Lifecycle Methods *** //
// *** WidgetPositionAlign Methods *** //
_uiSetAlign : function(node, points) {
// delegate UI alignment to Guide (parent) if this GuideStep is selected.
if (this.get('selected')) {
this.get('parent').align(node, points);
}
}
// *** Public Methods *** //
// *** Private Methods *** //
}, {
// *** Static *** //
ATTRS : {
strings : {
value : {
defLabel : 'Step '
}
},
/**
* @attribute label
* @type {String}
* @default "Step x"
* @description The short-name of the GuideStep which will be used in navigation.
* The initial value will be parsed from the title attribute of the .yui3-widget-hd element,
* or get assigned the default value: "Step " + (index + 1), e.g. "Step 1".
*/
label : {
validator : isString,
valueFn : function () {
return ( this.getString('defLabel') + (this.get('index') + 1) );
}
}
},
HTML_PARSER : {
label : function (srcNode) {
return srcNode.one(DOT+STD_SECTION_CLASS_NAMES[STD_HEADER]).get('title');
}
}
});
// *** Guide *** //
Guide = Y.Base.create(GUIDE, Y.Overlay, [Y.WidgetParent], {
// *** Prototype *** //
STEPS_TEMPLATE : '<ol></ol>',
NEXT_TEMPLATE : '<button></button>',
CLOSE_TEMPLATE : '<button></button>',
NAVIGATION_TEMPLATE : '<ol></ol>',
NAVIGATION_ITEM_TEMPLATE : '<li><a></a></li>',
_initialNextNodeContent : null,
// *** Lifecycle Methods *** //
initializer : function () {
this._initialNextNodeContent = this.get(NEXT_NODE).getContent();
},
renderUI : function () {
var boundingBox = this.get(BOUNDING_BOX);
// override node:hide/show to make visiblity:hidden/visible
boundingBox._hide = Y.bind(boundingBox.setStyle, boundingBox, 'visibility', 'hidden');
boundingBox._show = Y.bind(boundingBox.setStyle, boundingBox, 'visibility', 'visible');
// this is where GuideSteps are rendered.
this._childrenContainer = this.get(STEPS_NODE);
// Render navigation for GuideSteps.
if (this.get(NAVIGATION)) {
this._uiUpdateNavigation();
}
},
bindUI : function () {
var nav = this.get(NAVIGATION),
navNode = this.get(NAVIGATION_NODE),
nextNode = this.get(NEXT_NODE),
closeNode = this.get(CLOSE_NODE);
this.after('addChild', this._afterGuideStepAdd);
this.after('removeChild', this._afterGuideStepRemove);
this.after('selectionChange', this._afterGuideStepSelectionChange);
this.on(GUIDE_STEP+':contentUpdate', this._onGuideStepContentUpdate);
if (nextNode) {
nextNode.on('click', Y.bind(this._onNextClick, this));
}
if (closeNode) {
closeNode.on('click', Y.bind(this._onCloseClick, this));
}
if (nav && navNode) {
navNode.delegate('click', Y.bind(this._onNavigationStepClick, this), DOT+this.getClassName(NAVIGATION, STEP)+' a');
}
},
syncUI : function () {
// adding stuff to WidgetStdMod content areas.
if ( ! this.get('rendered')) {
this._insertGuideStdModContent();
}
// select the first GuideStep unless otherwise set.
if ( ! this.get('selection')) {
this.selectChild(0);
}
},
// *** Public Methods *** //
// *** Private Methods *** //
_insertGuideStdModContent : function () {
var bodyContent = this.getStdModNode(STD_BODY),
footerContent = this.getStdModNode(STD_FOOTER),
stepsNode = this.get(STEPS_NODE),
navNode = this.get(NAVIGATION_NODE),
nextNode = this.get(NEXT_NODE),
closeNode = this.get(CLOSE_NODE);
// Prepend navigation to bodyContent (if it’s not already)
if (this.get(NAVIGATION) && navNode && ! bodyContent.contains(navNode)) {
this.setStdModContent(STD_BODY, navNode, 'before');
}
// Append steps container to bodyContent (if it’s not already)
if ( ! bodyContent.contains(stepsNode)) {
this.setStdModContent(STD_BODY, stepsNode, 'after');
}
// Append next button to footerContent (if it’s not already)
if (nextNode && ! footerContent.contains(nextNode)) {
this.setStdModContent(STD_FOOTER, nextNode, 'after');
}
// Append close button to footerContent (if it’s not already)
if (closeNode && ! footerContent.contains(closeNode)) {
this.setStdModContent(STD_FOOTER, closeNode, 'after');
}
},
_defStepsNodeValueFn : function () {
return Y.Node.create(this.STEPS_TEMPLATE);
},
_setStepsNode : function (node) {
node = Y.one(node);
if (node) {
node.addClass(this.getClassName(STEPS));
}
return node;
},
_defNextNodeValueFn : function () {
return Y.Node.create(this.NEXT_TEMPLATE).set('text', this.getString('defNextLabel'));
},
_setNextNode : function (node) {
node = Y.one(node);
if (node) {
node.addClass(this.getClassName(NEXT));
}
return node;
},
_defCloseNodeValueFn : function () {
return Y.Node.create(this.CLOSE_TEMPLATE).set('text', this.getString('defCloseLabel'));
},
_setCloseNode : function (node) {
node = Y.one(node);
if (node) {
node.addClass(this.getClassName(CLOSE));
}
return node;
},
_defNavigationNodeValueFn : function () {
return ( this.get(NAVIGATION) ? Y.Node.create(this.NAVIGATION_TEMPLATE) : null );
},
_setNavigationNode : function (node) {
node = Y.one(node);
if (node) {
node.addClass(this.getClassName(NAVIGATION));
}
return node;
},
_uiUpdateNavigation : function () {
var nav = this.get(NAVIGATION),
navNode = this.get(NAVIGATION_NODE),
selection = this.get('selection'),
navStepClass = this.getClassName(NAVIGATION, STEP),
navStepSelectedClass = this.getClassName(NAVIGATION, STEP, SELECTED);
// we need both
if ( ! (nav && navNode)) { return; }
// clear out all the nav items
navNode.all(DOT+navStepClass).remove();
// add navigation items for each GuideStep
this.each(Y.bind(function(step){
var navItemNode = Y.Node.create(this.NAVIGATION_ITEM_TEMPLATE);
navItemNode
.addClass(navStepClass)
.one('a')
.set('href', '#' + step.get('id'))
.set('text', step.get('label'));
if (step === selection) {
navItemNode.addClass(navStepSelectedClass);
}
navNode.append(navItemNode);
}, this));
},
_uiUpdateNavigationSelected : function (prevStep, newStep) {
var nav = this.get(NAVIGATION),
navNode = this.get(NAVIGATION_NODE),
navStepClass = this.getClassName(NAVIGATION, STEP),
navStepSelectedClass = this.getClassName(NAVIGATION, STEP, SELECTED),
navItemNodes;
// we need both
if ( ! (nav && navNode)) { return; }
navItemNodes = navNode.all(DOT+navStepClass);
// remove selected class from prev step
if (prevStep) {
navItemNodes.item(this.indexOf(prevStep)).removeClass(navStepSelectedClass);
}
// add selected class to newly selected step
navItemNodes.item(this.indexOf(newStep)).addClass(navStepSelectedClass);
},
_transitionGuide : function (callback) {
var boundingBox = this.get(BOUNDING_BOX);
// Smooooth transition between steps
if (this.get('rendered')) {
boundingBox.hide('fadeOut', { duration: 0.2 }, Y.bind(function(){
callback.call(this);
boundingBox.show('fadeIn', { duration: 0.2 });
}, this));
} else {
callback.call(this);
}
},
_uiSetGuideStep : function (prevStep, newStep) {
// Hide all steps, but the old one so we can transition
this.each(function(step){
if ( ! prevStep || step !== prevStep) {
step.hide();
}
});
this._transitionGuide(function(){
var align = newStep.get('align'),
isLastStep;
// hide old show newly selected step
if (prevStep) {
prevStep.hide();
}
newStep.show();
// proxy selected steps alignment, or center Guide
if (align && align.points) {
this.align(align.node, align.points);
} else {
this.centered();
}
// check and set next button to 'Done' if we need to
if ( ! this.get(CIRCULAR)) {
isLastStep = this.indexOf(newStep) === this.size() - 1;
this.get(NEXT_NODE).setContent(isLastStep ? this.getString('defDoneLabel') : this._initialNextNodeContent);
}
// update navigation
this._uiUpdateNavigationSelected(prevStep, newStep);
});
},
_afterGuideStepAdd : function (e) {
this._uiUpdateNavigation();
},
_afterGuideStepRemove : function (e) {
this._uiUpdateNavigation();
},
_afterGuideStepSelectionChange : function (e) {
this._uiSetGuideStep(e.prevVal, e.newVal);
},
_onGuideStepContentUpdate : function (e) {
if (e.target === this.get('selection')) {
this.syncUI();
}
},
_onNextClick : function (e) {
var nextStep = this.get('selection').next(this.get(CIRCULAR));
if (nextStep) {
this.selectChild(nextStep.get('index'));
} else {
this.hide();
}
},
_onCloseClick : function (e) {
this.hide();
},
_onNavigationStepClick : function (e) {
var navStepSelector = DOT+this.getClassName(NAVIGATION, STEP),
navSteps = this.get(NAVIGATION_NODE).all(navStepSelector);
this.selectChild(navSteps.indexOf(e.currentTarget.ancestor(navStepSelector)));
e.preventDefault();
}
}, {
// *** Static *** //
ATTRS : {
strings : {
value : {
defNextLabel : 'Next',
defDoneLabel : 'Done',
defCloseLabel : 'Close'
}
},
// Override WidgetParent value
defaultChildType : {
value : GuideStep
},
// Override WidgetParent value and readOnly
multiple : {
value : false,
readOnly : true
},
/**
* @attribute circular
* @type {Boolean}
* @default false
* @description Whether or not the next button changes to a done button when on the last step.
*/
circular : {
value : false,
validator : isBoolean
},
/**
* @attribute stepsNode
* @type {Node}
* @default node
* @description The children container Node where the GuideSteps are rendered into.
*/
stepsNode : {
valueFn : '_defStepsNodeValueFn',
setter : '_setStepsNode',
writeOnce : true
},
/**
* @attribute nextNode
* @type {Node}
* @default node
* @description The button to move to the next GuideStep.
*/
nextNode : {
valueFn : '_defNextNodeValueFn',
setter : '_setNextNode',
writeOnce : true
},
/**
* @attribute closeNode
* @type {Node}
* @default node
* @description The button to close the Guide.
*/
closeNode : {
valueFn : '_defCloseNodeValueFn',
setter : '_setCloseNode',
writeOnce : true
},
/**
* @attribute navigation
* @type {Boolean}
* @default true
* @description Whether or not to render a navigation list for the Guide’s Steps.
*/
navigation : {
value : true,
validator : isBoolean,
initOnly : true
},
/**
* @attibute navigationNode
* @type {Node}
* @default node
* @description The Node for the navigation for the GuideSteps as a list of the GuideStep labels
*/
navigationNode : {
valueFn : '_defNavigationNodeValueFn',
setter : '_setNavigationNode',
writeOnce : true
}
},
HTML_PARSER : {
stepsNode : DOT+getCN(GUIDE, STEPS),
nextNode : DOT+getCN(GUIDE, NEXT),
closeNode : DOT+getCN(GUIDE, CLOSE),
navigationNode : DOT+getCN(GUIDE, NAVIGATION)
}
});
// *** Namespace *** //
Y.namespace('TTW');
Y.TTW.Guide = Guide;
Y.TTW.GuideStep = GuideStep;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment