var $ = require('../static'), utils = require('../utils'), isTag = utils.isTag, domEach = utils.domEach, hasOwn = Object.prototype.hasOwnProperty, camelCase = utils.camelCase, cssCase = utils.cssCase, rspace = /\s+/, dataAttrPrefix = 'data-', _ = { forEach: require('lodash.foreach'), extend: require('lodash.assignin'), some: require('lodash.some') }, // Lookup table for coercing string data-* attributes to their corresponding // JavaScript primitives primitives = { null: null, true: true, false: false }, // Attributes that are booleans rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, // Matches strings that look like JSON objects or arrays rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/; var getAttr = function(elem, name) { if (!elem || !isTag(elem)) return; if (!elem.attribs) { elem.attribs = {}; } // Return the entire attribs object if no attribute specified if (!name) { return elem.attribs; } if (hasOwn.call(elem.attribs, name)) { // Get the (decoded) attribute return rboolean.test(name) ? name : elem.attribs[name]; } // Mimic the DOM and return text content as value for `option's` if (elem.name === 'option' && name === 'value') { return $.text(elem.children); } // Mimic DOM with default value for radios/checkboxes if (elem.name === 'input' && (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') && name === 'value') { return 'on'; } }; var setAttr = function(el, name, value) { if (value === null) { removeAttribute(el, name); } else { el.attribs[name] = value+''; } }; exports.attr = function(name, value) { // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { return domEach(this, function(i, el) { setAttr(el, name, value.call(el, i, el.attribs[name])); }); } return domEach(this, function(i, el) { if (!isTag(el)) return; if (typeof name === 'object') { _.forEach(name, function(value, name) { setAttr(el, name, value); }); } else { setAttr(el, name, value); } }); } return getAttr(this[0], name); }; var getProp = function (el, name) { if (!el || !isTag(el)) return; return el.hasOwnProperty(name) ? el[name] : rboolean.test(name) ? getAttr(el, name) !== undefined : getAttr(el, name); }; var setProp = function (el, name, value) { el[name] = rboolean.test(name) ? !!value : value; }; exports.prop = function (name, value) { var i = 0, property; if (typeof name === 'string' && value === undefined) { switch (name) { case 'style': property = this.css(); _.forEach(property, function (v, p) { property[i++] = p; }); property.length = i; break; case 'tagName': case 'nodeName': property = this[0].name.toUpperCase(); break; default: property = getProp(this[0], name); } return property; } if (typeof name === 'object' || value !== undefined) { if (typeof value === 'function') { return domEach(this, function(i, el) { setProp(el, name, value.call(el, i, getProp(el, name))); }); } return domEach(this, function(i, el) { if (!isTag(el)) return; if (typeof name === 'object') { _.forEach(name, function(val, name) { setProp(el, name, val); }); } else { setProp(el, name, value); } }); } }; var setData = function(el, name, value) { if (!el.data) { el.data = {}; } if (typeof name === 'object') return _.extend(el.data, name); if (typeof name === 'string' && value !== undefined) { el.data[name] = value; } else if (typeof name === 'object') { _.extend(el.data, name); } }; // Read the specified attribute from the equivalent HTML5 `data-*` attribute, // and (if present) cache the value in the node's internal data store. If no // attribute name is specified, read *all* HTML5 `data-*` attributes in this // manner. var readData = function(el, name) { var readAll = arguments.length === 1; var domNames, domName, jsNames, jsName, value, idx, length; if (readAll) { domNames = Object.keys(el.attribs).filter(function(attrName) { return attrName.slice(0, dataAttrPrefix.length) === dataAttrPrefix; }); jsNames = domNames.map(function(domName) { return camelCase(domName.slice(dataAttrPrefix.length)); }); } else { domNames = [dataAttrPrefix + cssCase(name)]; jsNames = [name]; } for (idx = 0, length = domNames.length; idx < length; ++idx) { domName = domNames[idx]; jsName = jsNames[idx]; if (hasOwn.call(el.attribs, domName)) { value = el.attribs[domName]; if (hasOwn.call(primitives, value)) { value = primitives[value]; } else if (value === String(Number(value))) { value = Number(value); } else if (rbrace.test(value)) { try { value = JSON.parse(value); } catch(e){ } } el.data[jsName] = value; } } return readAll ? el.data : value; }; exports.data = function(name, value) { var elem = this[0]; if (!elem || !isTag(elem)) return; if (!elem.data) { elem.data = {}; } // Return the entire data object if no data specified if (!name) { return readData(elem); } // Set the value (with attr map support) if (typeof name === 'object' || value !== undefined) { domEach(this, function(i, el) { setData(el, name, value); }); return this; } else if (hasOwn.call(elem.data, name)) { return elem.data[name]; } return readData(elem, name); }; /** * Get the value of an element */ exports.val = function(value) { var querying = arguments.length === 0, element = this[0]; if(!element) return; switch (element.name) { case 'textarea': return this.text(value); case 'input': switch (this.attr('type')) { case 'radio': if (querying) { return this.attr('value'); } else { this.attr('value', value); return this; } break; default: return this.attr('value', value); } return; case 'select': var option = this.find('option:selected'), returnValue; if (option === undefined) return undefined; if (!querying) { if (!this.attr().hasOwnProperty('multiple') && typeof value == 'object') { return this; } if (typeof value != 'object') { value = [value]; } this.find('option').removeAttr('selected'); for (var i = 0; i < value.length; i++) { this.find('option[value="' + value[i] + '"]').attr('selected', ''); } return this; } returnValue = option.attr('value'); if (this.attr().hasOwnProperty('multiple')) { returnValue = []; domEach(option, function(i, el) { returnValue.push(getAttr(el, 'value')); }); } return returnValue; case 'option': if (!querying) { this.attr('value', value); return this; } return this.attr('value'); } }; /** * Remove an attribute */ var removeAttribute = function(elem, name) { if (!elem.attribs || !hasOwn.call(elem.attribs, name)) return; delete elem.attribs[name]; }; exports.removeAttr = function(name) { domEach(this, function(i, elem) { removeAttribute(elem, name); }); return this; }; exports.hasClass = function(className) { return _.some(this, function(elem) { var attrs = elem.attribs, clazz = attrs && attrs['class'], idx = -1, end; if (clazz) { while ((idx = clazz.indexOf(className, idx+1)) > -1) { end = idx + className.length; if ((idx === 0 || rspace.test(clazz[idx-1])) && (end === clazz.length || rspace.test(clazz[end]))) { return true; } } } }); }; exports.addClass = function(value) { // Support functions if (typeof value === 'function') { return domEach(this, function(i, el) { var className = el.attribs['class'] || ''; exports.addClass.call([el], value.call(el, i, className)); }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; var classNames = value.split(rspace), numElements = this.length; for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on if (!isTag(this[i])) continue; // If we don't already have classes var className = getAttr(this[i], 'class'), numClasses, setClass; if (!className) { setAttr(this[i], 'class', classNames.join(' ').trim()); } else { setClass = ' ' + className + ' '; numClasses = classNames.length; // Check if class already exists for (var j = 0; j < numClasses; j++) { var appendClass = classNames[j] + ' '; if (setClass.indexOf(' ' + appendClass) < 0) setClass += appendClass; } setAttr(this[i], 'class', setClass.trim()); } } return this; }; var splitClass = function(className) { return className ? className.trim().split(rspace) : []; }; exports.removeClass = function(value) { var classes, numClasses, removeAll; // Handle if value is a function if (typeof value === 'function') { return domEach(this, function(i, el) { exports.removeClass.call( [el], value.call(el, i, el.attribs['class'] || '') ); }); } classes = splitClass(value); numClasses = classes.length; removeAll = arguments.length === 0; return domEach(this, function(i, el) { if (!isTag(el)) return; if (removeAll) { // Short circuit the remove all case as this is the nice one el.attribs.class = ''; } else { var elClasses = splitClass(el.attribs.class), index, changed; for (var j = 0; j < numClasses; j++) { index = elClasses.indexOf(classes[j]); if (index >= 0) { elClasses.splice(index, 1); changed = true; // We have to do another pass to ensure that there are not duplicate // classes listed j--; } } if (changed) { el.attribs.class = elClasses.join(' '); } } }); }; exports.toggleClass = function(value, stateVal) { // Support functions if (typeof value === 'function') { return domEach(this, function(i, el) { exports.toggleClass.call( [el], value.call(el, i, el.attribs['class'] || '', stateVal), stateVal ); }); } // Return if no value or not a string or function if (!value || typeof value !== 'string') return this; var classNames = value.split(rspace), numClasses = classNames.length, state = typeof stateVal === 'boolean' ? stateVal ? 1 : -1 : 0, numElements = this.length, elementClasses, index; for (var i = 0; i < numElements; i++) { // If selected element isn't a tag, move on if (!isTag(this[i])) continue; elementClasses = splitClass(this[i].attribs.class); // Check if class already exists for (var j = 0; j < numClasses; j++) { // Check if the class name is currently defined index = elementClasses.indexOf(classNames[j]); // Add if stateValue === true or we are toggling and there is no value if (state >= 0 && index < 0) { elementClasses.push(classNames[j]); } else if (state <= 0 && index >= 0) { // Otherwise remove but only if the item exists elementClasses.splice(index, 1); } } this[i].attribs.class = elementClasses.join(' '); } return this; }; exports.is = function (selector) { if (selector) { return this.filter(selector).length > 0; } return false; };