var _ = require('lodash'), utils = require('../utils'), isTag = utils.isTag, domEach = utils.domEach, hasOwn = Object.prototype.hasOwnProperty, camelCase = utils.camelCase, cssCase = utils.cssCase, rspace = /\s+/, dataAttrPrefix = 'data-', // 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 elem.attribs[name]; } }; var setAttr = function(el, name, value) { if (value === null) { removeAttribute(el, name); } else { el.attribs[name] = value+''; } }; var attr = 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') { _.each(name, function(name, key) { el.attribs[key] = name+''; }); } else { setAttr(el, name, value); } }); } return getAttr(this[0], name); }; var setData = function(el, name, value) { if (typeof name === 'object') return _.extend(el.data, name); if (typeof name === 'string' && value !== undefined) { el.data[name] = value; } else if (typeof name === 'object') { _.exend(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)) { value = JSON.parse(value); } el.data[jsName] = value; } } return readAll ? el.data : value; }; var data = 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 */ var val = 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': var queryString = 'input[type=radio][name=' + this.attr('name') + ']:checked'; var parentEl, root; // Go up until we hit a form or root parentEl = this.closest('form'); if (parentEl.length === 0) { root = (this.parents().last()[0] || this[0]).root; parentEl = this._make(root); } if (querying) { return parentEl.find(queryString).attr('value'); } else { parentEl.find(':checked').removeAttr('checked'); parentEl.find('input[type=radio][value="' + value + '"]').attr('checked', ''); 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(el.attribs.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; if (name === elem.attribs[name] && rboolean.test(elem.attribs[name])) elem.attribs[name] = false; else delete elem.attribs[name]; }; var removeAttr = exports.removeAttr = function(name) { domEach(this, function(i, elem) { removeAttribute(elem, name); }); return this; }; var hasClass = exports.hasClass = function(className) { return _.any(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; } } } }); }; var addClass = exports.addClass = function(value) { // Support functions if (typeof value === 'function') { return domEach(this, function(i, el) { var className = el.attribs['class'] || ''; 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)) setClass += appendClass; } setAttr(this[i], 'class', setClass.trim()); } } return this; }; var splitClass = function(className) { return className ? className.trim().split(rspace) : []; }; var removeClass = exports.removeClass = function(value) { var classes, numClasses, removeAll; // Handle if value is a function if (typeof value === 'function') { return domEach(this, function(i, el) { 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(' '); } } }); }; var toggleClass = exports.toggleClass = function(value, stateVal) { // Support functions if (typeof value === 'function') { return domEach(this, function(i, el) { 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; }; var is = exports.is = function (selector) { if (selector) { return this.filter(selector).length > 0; } return false; };