/*jslint white: true, nomen: true, maxerr: 50, indent: 4, regexp:true */
/*global YUI*/
/**
* An extension for Widget to create markup from templates,
* create CSS classNames, locating elements,
* assist in attaching events to UI elements and to reflect attribute changes into the UI.
* All of its members are either protected or private.
* Developers using MakeNode should use only those marked protected.
* Enable the Show Protected checkbox to see them.
* @module makenode
* @class MakeNode
*/
YUI.add('makenode',function(Y) {
"use strict";
var WS = /\s+/,
NODE = 'Node',
DOT = '.',
BBX = 'boundingBox',
Lang = Y.Lang,
DUPLICATE = ' for "{name}" defined in class {recentDef} also defined in class {prevDef}',
/**
* Creates CSS classNames from suffixes listed in _CLASS_NAMES
,
* stores them in this._classNames
.
* Concatenates _ATTRS_2_UI
into _UI_ATTRS
.
* Sets listeners to render and destroy events to attach/detach UI events
* @constructor
*/
MakeNode = function () {
this._makeClassNames();
this._concatUIAttrs();
this.after('render', this._attachEvents, this);
this.after('destroy', this._detachEvents, this);
};
MakeNode.prototype = {
/**
* Contains a hash of CSS classNames generated from the entries in _CLASS_NAMES
* indexed by those same values.
* It will also have the following entries added automatically:
.
The className for the boundingBoxcontent
The className for the contentBoxHEADER
The className for the header section of a StdMod if Y.WidgetStdMod has been loadedBODY
The className for the body section of a StdMod if Y.WidgetStdMod has been loadedFOOTER
The className for the footer section of a StdMod if Y.WidgetStdMod has been loaded{m}
placeholder.
* It recognizes numbers, true
, false
, null
and double quoted strings, each separated by whitespace.
* It skips over anything else.
* @method _parseMakeNodeArgs
* @param arg {String} String to be parsed for arguments
* @return {Array} Array of arguments found, each converted to its proper data type
* @private
*/
_parseMakeNodeArgs: function (arg) {
var regexp = /^(?:([ \t]+)|("[^"\\]*(?:\\.[^"\\]*)*")|(true)|(false)|(null)|([\-+]?[0-9]*(?:\.[0-9]+)?))/,
args = [],
matcher = function (match, i) {
if (match !== undefined && i) {
switch (i) {
case 1:
break;
case 2:
args.push(match.substr(1, match.length - 2).replace('\\"','"'));
break;
case 3:
args.push(true);
break;
case 4:
args.push(false);
break;
case 5:
args.push(null);
break;
case 6:
if (match) {
args.push(parseFloat(match));
} else {
// The last parenthesis of the RegExp succeeds on anything else since both the integer and decimal
// parts of a number are optional, however, it captures nothing, just an empty string.
// So, any string other than true, false, null or a properly quoted string will end up here.
// I just consume it one character at a time to avoid looping forever on errors.
arg = arg.substr(1);
}
break;
}
arg = arg.substr(match.length);
return true;
}
};
while (arg.length) {
Y.some(regexp.exec(arg), matcher);
}
return args;
},
/**
* Processes the template given and returns a Y.Node
instance.
* @method _makeNode
* @param template {String} (optional) Template to process.
* If missing, it will use the first static _TEMPLATE
property found in the inheritance chain.
* @param extras {Object} (optional) Hash of extra values to replace into the template, beyond MakeNode's processing codes.
* @return {Y.Node} Instance of Y.Node
produced from the template
* @protected
*/
_makeNode: function(template, extras) {
if (!template) {
Y.some(this._getClasses(), function (c) {
template = c._TEMPLATE;
if (template) {
return true;
}
});
}
return Y.Node.create(this._substitute(template, extras));
},
/**
* Processes the given template and returns a string
* @method _substitute
* @param template {String} (optional) Template to process.
* If missing, it will use the first static _TEMPLATE
property found in the inheritance chain.
* @param extras {Object} (optional) Hash of extra values to replace into the template, beyond MakeNode's processing codes.
* @return {String} Template with the placeholders replaced.
* @protected
*/
_substitute: function (template, extras) {
var fn;
return Y.substitute(template , extras || {}, Y.bind(function (key, suggested, arg) {
if (arg) {
fn = this._templateHandlers[key.toLowerCase()];
if (fn) {
return fn.call(this, arg);
}
}
return suggested;
}, this),true);
},
/**
* Locates the nodes with the CSS classNames listed in the this._classNames
property,
* or those specifically requested in its arguments and stores references to them
* in properties named after each className key, prefixed with an underscore
* and followed by "Node"
.
* @method _locateNodes
* @param arg1,.... {String} (optional) If given, list of className heys of the nodes to be located.
* If missing, all the classNames stored in this._classNames
will be located.
* @protected
*/
_locateNodes: function () {
var bbx = this.get(BBX), el;
if (arguments.length) {
Y.each(arguments, function( name) {
el = bbx.one(DOT + this._classNames[name]);
if (el) {
this['_' + name + NODE] = el;
}
}, this);
} else {
Y.each(this._classNames, function(selector, name) {
el = bbx.one(DOT + selector);
if (el) {
this['_' + name + NODE] = el;
}
}, this);
}
},
/**
* Looks for static properties called _CLASS_NAMES
in each of the classes of the inheritance chain
* and generates CSS classNames based on the _cssPrefix
of each class and each of the suffixes listed in each them.
* The classNames generated will be stored in this._classNames
indexed by the suffix.
* It will also store the classNames of the boundingBox ( . )and the contentBox ( content ).
* If the WidgetStdMod is used, it will also add the classNames for the three sections ( HEADER, BODY, FOOTER )
* @method _makeClassNames
* @private
*/
_makeClassNames: function () {
var YCM = Y.ClassNameManager.getClassName,
defined = {},
cns = this._classNames = {};
Y.each(this._getClasses(), function (c) {
if (c._CLASS_NAMES) {
Y.each(c._CLASS_NAMES, function(name) {
if (defined[name]) {
Y.log(Y.substitute('ClassName' + DUPLICATE, {name:name, recentDef: defined[name], prevDef: c.NAME}), 'warn', 'MakeNode');
} else {
cns[name] = YCM(c.NAME.toLowerCase(), name);
defined[name] = c.NAME;
}
});
}
});
cns.content = (cns[DOT] = YCM(this.constructor.NAME.toLowerCase())) + '-content';
if (this.getStdModNode) {
cns.HEADER = 'yui3-widget-hd';
cns.BODY = 'yui3-widget-bd';
cns.FOOTER = 'yui3-widget-ft';
}
},
/**
* Concatenates the entries of the _ATTRS_2_UI
static property of each class in the inheritance chain
* into this instance _UI_ATTRS property for the benefit or Widget. See Widget._UI_ATTRS
* @method _concatUIAttrs
* @private
*/
_concatUIAttrs: function () {
var defined, u, U = {};
Y.each(['BIND','SYNC'], function (which) {
defined = {};
Y.each(this._UI_ATTRS[which], function (name) {
defined[name] = 'Widget';
});
Y.each(this._getClasses(), function (c) {
u = c._ATTRS_2_UI;
if (u) {
Y.each(Y.Array(u[which]), function (name) {
if (defined[name]) {
Y.log(Y.substitute('UI ' + which + ' Attribute' + DUPLICATE, {name:name, recentDef: defined[name], prevDef: c.NAME}), 'warn', 'MakeNode');
} else {
defined[name] = c.NAME;
}
});
}
});
U[which]= Y.Object.keys(defined);
},this);
this._UI_ATTRS = U;
},
/**
* Attaches the events listed in the _EVENTS
static property of each class in the inheritance chain.
* @method _attachEvents
* @private
*/
_attachEvents: function () {
/*jslint confusion: true */
var bbx = this.get(BBX),
selector,
self = this,
eh = [];
Y.each(this._getClasses(), function (c) {
Y.each (c._EVENTS || {}, function (handlers, key) {
selector = {
DOT:bbx,
'..':bbx.get('ownerDocument'),
THIS:self,
Y:Y
}[key] || DOT + this._classNames[key];
Y.each(handlers, function (handler, type) {
if (Lang.isString(handler)) {
handler = {fn:handler};
}
if (Lang.isObject(handler) && handler.hasOwnProperty('fn')) {
if (Lang.isString(selector)) {
if (type==='key') {
eh.push(bbx.delegate(type, self[handler.fn], handler.args, selector, self));
} else {
eh.push(bbx.delegate(type, self[handler.fn], selector, self, handler.args));
}
} else {
if (selector === self || selector === Y) {
eh.push(selector.after(type, self[handler.fn], self, handler.args));
} else {
if (type==='key') {
eh.push(Y.on(type, self[handler.fn], selector, handler.args, self));
} else {
eh.push(Y.on(type, self[handler.fn], selector, self, handler.args));
}
}
}
}
});
}, this);
}, this);
this._eventHandles = eh;
},
/**
* Detaches all the events created by _attachEvents
* @method _detachEvents
* @private
*/
_detachEvents: function () {
Y.each(this._eventHandles, function (handle) {
handle.detach();
});
}
};
/**
* ** This is a documentation entry only.
* This property is not defined in this file, it should be defined by the developer. **_makeNode
when none is explicitly provided.Y.substitute
,
* whose values will be extracted from the second argument to _makeNode
.
* The processing codes are:
{@ attributeName}
configuration attribute values{p propertyName}
instance property values{m methodName arg1 arg2 ....}
return value from instance method.
The m
code should be followed by the
method name and any number of arguments. The
placeholder is replaced by the return value or the named method.{c classNameKey}
CSS className generated from the _CLASS_NAMES
static property {s key}
string from the strings
attribute, using key
as the sub-attribute.{? arg1 arg2 arg3}
If arg1 evaluates to true it returns arg2 otherwise arg3.
Argument arg1 is usually a nested placeholder.{1 arg1 arg2 arg3}
If arg1 is 1 it returns arg2 otherwise arg3. Used to produce singular/plural text.
Argument arg1 is usually a nested placeholder.{}
any other value will be handled just like Y.substitute
does. this._classNames
,
* as the argument to the {c}
template placeholder
* and as keys for the entries in the _EVENTS
property.
* They are also used by _locateNodes
to create the private properties that hold
* references to the nodes created.
* @property _CLASS_NAMES
* @type [String]
* @static
* @protected
*/
/**
* ** This is a documentation entry only.
* This property is not defined in this file, it should be defined by the developer. **BIND
and SYNC
, each
* containing the name of an attribute or an array of names of attributes.
* Those listed in BIND
will have listeners attached to their change event
* so every such change is refreshed in the UI.
* Those listed in SYNC
will be refreshed when the UI is rendered.
* For each entry in either list there should be a method named using the _uiSet
prefix, followed by
* the name of the attribute, with its first character in uppercase.
* This function will receive the value to be set and the source of the change.
* @property _ATTRS_2_UI
* @type Object
* @static
* @protected
*/
/**
* ** This is a documentation entry only.
* This property is not defined in this file, it should be defined by the developer. **_CLASS_NAMES
property. "."
identifies the boundingBox of the Widget, "content"
its contextBox, and a ".."
identifies the document
* where the component is in.
* If the Y.WidgetStdMod extension is used the "HEADER"
, "BODY"
and "FOOTER"
identifiers will also be available."key"
, "mousedown"
, etc)
* as the key to each handler.fn
with a string with the name of the instance method and an args
property with extra
* arguments for the listener, such as a key descriptor for key
events
* @property _EVENTS
* @type Object
* @static
* @protected
*/
Y.MakeNode = MakeNode;
},0.9,{
requires:['substitute','classnamemanager']
});