/** * @ignore * dom-attr * @author yiminghe@gmail.com, lifesinger@gmail.com */ KISSY.add('dom/base/attr', function (S, Dom, undefined) { var doc = S.Env.host.document, NodeType = Dom.NodeType, docElement = doc && doc.documentElement, EMPTY = '', nodeName = Dom.nodeName, R_BOOLEAN = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, R_FOCUSABLE = /^(?:button|input|object|select|textarea)$/i, R_CLICKABLE = /^a(?:rea)?$/i, R_INVALID_CHAR = /:|^on/, R_RETURN = /\r/g, attrFix = { }, attrFn = { val: 1, css: 1, html: 1, text: 1, data: 1, width: 1, height: 1, offset: 1, scrollTop: 1, scrollLeft: 1 }, attrHooks = { // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ tabindex: { get: function (el) { // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set var attributeNode = el.getAttributeNode('tabindex'); return attributeNode && attributeNode.specified ? parseInt(attributeNode.value, 10) : R_FOCUSABLE.test(el.nodeName) || R_CLICKABLE.test(el.nodeName) && el.href ? 0 : undefined; } } }, propFix = { 'hidefocus': 'hideFocus', tabindex: 'tabIndex', readonly: 'readOnly', 'for': 'htmlFor', 'class': 'className', maxlength: 'maxLength', 'cellspacing': 'cellSpacing', 'cellpadding': 'cellPadding', rowspan: 'rowSpan', colspan: 'colSpan', usemap: 'useMap', 'frameborder': 'frameBorder', 'contenteditable': 'contentEditable' }, // Hook for boolean attributes // if bool is false // - standard browser returns null // - ie<8 return false // - norm to undefined boolHook = { get: function (elem, name) { // 转发到 prop 方法 return Dom.prop(elem, name) ? // 根据 w3c attribute , true 时返回属性名字符串 name.toLowerCase() : undefined; }, set: function (elem, value, name) { var propName; if (value === false) { // Remove boolean attributes when set to false Dom.removeAttr(elem, name); } else { // 直接设置 true,因为这是 bool 类属性 propName = propFix[ name ] || name; if (propName in elem) { // Only set the IDL specifically if it already exists on the element elem[ propName ] = true; } elem.setAttribute(name, name.toLowerCase()); } return name; } }, propHooks = { }, // get attribute value from attribute node, only for ie attrNodeHook = { }, valHooks = { select: { // fix for multiple select get: function (elem) { var index = elem.selectedIndex, options = elem.options, ret, i, len, one = (String(elem.type) === 'select-one'); // Nothing was selected if (index < 0) { return null; } else if (one) { return Dom.val(options[index]); } // Loop through all the selected options ret = []; i = 0; len = options.length; for (; i < len; ++i) { if (options[i].selected) { ret.push(Dom.val(options[i])); } } // Multi-Selects return an array return ret; }, set: function (elem, value) { var values = S.makeArray(value), opts = elem.options; S.each(opts, function (opt) { opt.selected = S.inArray(Dom.val(opt), values); }); if (!values.length) { elem.selectedIndex = -1; } return values; } } }; // Radios and checkboxes getter/setter S.each(['radio', 'checkbox'], function (r) { valHooks[r] = { get: function (elem) { // Handle the case where in Webkit '' is returned instead of 'on' // if a value isn't specified return elem.getAttribute('value') === null ? 'on' : elem.value; }, set: function (elem, value) { if (S.isArray(value)) { return elem.checked = S.inArray(Dom.val(elem), value); } return undefined; } }; }); // IE7- 下,需要用 cssText 来获取 // 所有浏览器统一下, attr('style') 标准浏览器也不是 undefined attrHooks['style'] = { get: function (el) { return el.style.cssText; } }; function getProp(elem, name) { name = propFix[name] || name; var hook = propHooks[name]; if (hook && hook.get) { return hook.get(elem, name); } else { return elem[name]; } } S.mix(Dom, /** * @override KISSY.DOM * @class * @singleton */ { _valHooks: valHooks, _propFix: propFix, _attrHooks: attrHooks, _propHooks: propHooks, _attrNodeHook: attrNodeHook, _attrFix: attrFix, /** * Get the value of a property for the first element in the set of matched elements. * or * Set one or more properties for the set of matched elements. * @param {HTMLElement[]|String|HTMLElement} selector matched elements * @param {String|Object} name * The name of the property to set. * or * A map of property-value pairs to set. * @param [value] A value to set for the property. * @return {String|undefined|Boolean} */ prop: function (selector, name, value) { var elems = Dom.query(selector), i, elem, hook; // supports hash if (S.isPlainObject(name)) { S.each(name, function (v, k) { Dom.prop(elems, k, v); }); return undefined; } // Try to normalize/fix the name name = propFix[ name ] || name; hook = propHooks[ name ]; if (value !== undefined) { for (i = elems.length - 1; i >= 0; i--) { elem = elems[i]; if (hook && hook.set) { hook.set(elem, value, name); } else { elem[ name ] = value; } } } else { if (elems.length) { return getProp(elems[0], name); } } return undefined; }, /** * Whether one of the matched elements has specified property name * @param {HTMLElement[]|String|HTMLElement} selector 元素 * @param {String} name The name of property to test * @return {Boolean} */ hasProp: function (selector, name) { var elems = Dom.query(selector), i, len = elems.length, el; for (i = 0; i < len; i++) { el = elems[i]; if (getProp(el, name) !== undefined) { return true; } } return false; }, /** * Remove a property for the set of matched elements. * @param {HTMLElement[]|String|HTMLElement} selector matched elements * @param {String} name The name of the property to remove. */ removeProp: function (selector, name) { name = propFix[ name ] || name; var elems = Dom.query(selector), i, el; for (i = elems.length - 1; i >= 0; i--) { el = elems[i]; try { el[ name ] = undefined; delete el[ name ]; } catch (e) { } } }, /** * Get the value of an attribute for the first element in the set of matched elements. * or * Set one or more attributes for the set of matched elements. * @param {HTMLElement[]|HTMLElement|String} selector matched elements * @param {String|Object} name The name of the attribute to set. or A map of attribute-value pairs to set. * @param [val] A value to set for the attribute. * @param [pass] internal use by anim * @return {String|undefined} */ attr: function (selector, name, val, /*internal use by anim/fx*/pass) { /* Hazards From Caja Note: - In IE[67], el.setAttribute doesn't work for attributes like 'class' or 'for'. IE[67] expects you to set 'className' or 'htmlFor'. Caja use setAttributeNode solves this problem. - In IE[67], <input> elements can shadow attributes. If el is a form that contains an <input> named x, then el.setAttribute(x, y) will set x's value rather than setting el's attribute. Using setAttributeNode solves this problem. - In IE[67], the style attribute can only be modified by setting el.style.cssText. Neither setAttribute nor setAttributeNode will work. el.style.cssText isn't bullet-proof, since it can be shadowed by <input> elements. - In IE[67], you can never change the type of an <button> element. setAttribute('type') silently fails, but setAttributeNode throws an exception. caja : the silent failure. KISSY throws error. - In IE[67], you can never change the type of an <input> element. setAttribute('type') throws an exception. We want the exception. - In IE[67], setAttribute is case-sensitive, unless you pass 0 as a 3rd argument. setAttributeNode is case-insensitive. - Trying to set an invalid name like ':' is supposed to throw an error. In IE[678] and Opera 10, it fails without an error. */ var els = Dom.query(selector), attrNormalizer, i, el = els[0], ret; // supports hash if (S.isPlainObject(name)) { pass = val; for (var k in name) { Dom.attr(els, k, name[k], pass); } return undefined; } // attr functions if (pass && attrFn[name]) { return Dom[name](selector, val); } // scrollLeft name = name.toLowerCase(); if (pass && attrFn[name]) { return Dom[name](selector, val); } // custom attrs name = attrFix[name] || name; if (R_BOOLEAN.test(name)) { attrNormalizer = boolHook; } // only old ie? else if (R_INVALID_CHAR.test(name)) { attrNormalizer = attrNodeHook; } else { attrNormalizer = attrHooks[name]; } if (val === undefined) { if (el && el.nodeType === NodeType.ELEMENT_NODE) { // browsers index elements by id/name on forms, give priority to attributes. if (nodeName(el) == 'form') { attrNormalizer = attrNodeHook; } if (attrNormalizer && attrNormalizer.get) { return attrNormalizer.get(el, name); } ret = el.getAttribute(name); if (ret === "") { var attrNode = el.getAttributeNode(name); if (!attrNode || !attrNode.specified) { return undefined; } } // standard browser non-existing attribute return null // ie<8 will return undefined , because it return property // so norm to undefined return ret === null ? undefined : ret; } } else { for (i = els.length - 1; i >= 0; i--) { el = els[i]; if (el && el.nodeType === NodeType.ELEMENT_NODE) { if (nodeName(el) == 'form') { attrNormalizer = attrNodeHook; } if (attrNormalizer && attrNormalizer.set) { attrNormalizer.set(el, val, name); } else { // convert the value to a string (all browsers do this but IE) el.setAttribute(name, EMPTY + val); } } } } return undefined; }, /** * Remove an attribute from each element in the set of matched elements. * @param {HTMLElement[]|String} selector matched elements * @param {String} name An attribute to remove */ removeAttr: function (selector, name) { name = name.toLowerCase(); name = attrFix[name] || name; var els = Dom.query(selector), propName, el, i; for (i = els.length - 1; i >= 0; i--) { el = els[i]; if (el.nodeType == NodeType.ELEMENT_NODE) { el.removeAttribute(name); // Set corresponding property to false for boolean attributes if (R_BOOLEAN.test(name) && (propName = propFix[ name ] || name) in el) { el[ propName ] = false; } } } }, /** * Whether one of the matched elements has specified attribute * @method * @param {HTMLElement[]|String} selector matched elements * @param {String} name The attribute to be tested * @return {Boolean} */ hasAttr: docElement && !docElement.hasAttribute ? function (selector, name) { name = name.toLowerCase(); var elems = Dom.query(selector), i, el, attrNode; // from ppk :http://www.quirksmode.org/dom/w3c_core.html // IE5-7 doesn't return the value of a style attribute. // var $attr = el.attributes[name]; for (i = 0; i < elems.length; i++) { el = elems[i]; attrNode = el.getAttributeNode(name); if (attrNode && attrNode.specified) { return true; } } return false; } : function (selector, name) { var elems = Dom.query(selector), i, len = elems.length; for (i = 0; i < len; i++) { //使用原生实现 if (elems[i].hasAttribute(name)) { return true; } } return false; }, /** * Get the current value of the first element in the set of matched elements. * or * Set the value of each element in the set of matched elements. * @param {HTMLElement[]|String} selector matched elements * @param {String|String[]} [value] A string of text or an array of strings corresponding to the value of each matched element to set as selected/checked. * @return {undefined|String|String[]|Number} */ val: function (selector, value) { var hook, ret, elem, els, i, val; //getter if (value === undefined) { elem = Dom.get(selector); if (elem) { hook = valHooks[ nodeName(elem) ] || valHooks[ elem.type ]; if (hook && 'get' in hook && (ret = hook.get(elem, 'value')) !== undefined) { return ret; } ret = elem.value; return typeof ret === 'string' ? // handle most common string cases ret.replace(R_RETURN, '') : // handle cases where value is null/undefined or number ret == null ? '' : ret; } return undefined; } els = Dom.query(selector); for (i = els.length - 1; i >= 0; i--) { elem = els[i]; if (elem.nodeType !== 1) { return undefined; } val = value; // Treat null/undefined as ''; convert numbers to string if (val == null) { val = ''; } else if (typeof val === 'number') { val += ''; } else if (S.isArray(val)) { val = S.map(val, function (value) { return value == null ? '' : value + ''; }); } hook = valHooks[ nodeName(elem)] || valHooks[ elem.type ]; // If set returns undefined, fall back to normal setting if (!hook || !('set' in hook) || hook.set(elem, val, 'value') === undefined) { elem.value = val; } } return undefined; }, /** * Get the combined text contents of each element in the set of matched elements, including their descendants. * or * Set the content of each element in the set of matched elements to the specified text. * @param {HTMLElement[]|HTMLElement|String} selector matched elements * @param {String} [val] A string of text to set as the content of each matched element. * @return {String|undefined} */ text: function (selector, val) { var el, els, i, nodeType; // getter if (val === undefined) { // supports css selector/Node/NodeList el = Dom.get(selector); return Dom._getText(el); } else { els = Dom.query(selector); for (i = els.length - 1; i >= 0; i--) { el = els[i]; nodeType = el.nodeType; if (nodeType == NodeType.ELEMENT_NODE) { Dom.empty(el); el.appendChild(el.ownerDocument.createTextNode(val)); } else if (nodeType == NodeType.TEXT_NODE || nodeType == NodeType.CDATA_SECTION_NODE) { el.nodeValue = val; } } } return undefined; }, _getText: function (el) { return el.textContent; } }); return Dom; }, { requires: ['./api'] }); /* NOTES: yiminghe@gmail.com: 2013-03-19 - boolean property 和 attribute ie 和其他浏览器不一致,统一为类似 ie8: - attr('checked',string) == .checked=true setAttribute('checked','checked') // ie8 相同 setAttribute() - attr('checked',false) == removeAttr('check') // ie8 不同, setAttribute ie8 相当于 .checked=true setAttribute('checked','checked') - removeAttr('checked') == .checked=false removeAttribute('checked') // ie8 removeAttribute 相同 yiminghe@gmail.com: 2012-11-27 - 拆分 ie attr,条件加载 yiminghe@gmail.com:2011-06-03 - 借鉴 jquery 1.6,理清 attribute 与 property yiminghe@gmail.com:2011-01-28 - 处理 tabindex,顺便重构 2010.03 - 在 jquery/support.js 中,special attrs 里还有 maxlength, cellspacing, rowspan, colspan, usemap, frameborder, 但测试发现,在 Grade-A 级浏览器中 并无兼容性问题。 - 当 colspan/rowspan 属性值设置有误时,ie7- 会自动纠正,和 href 一样,需要传递 第 2 个参数来解决。jQuery 未考虑,存在兼容性 bug. - jQuery 考虑了未显式设定 tabindex 时引发的兼容问题,kissy 里忽略(太不常用了) - jquery/attributes.js: Safari mis-reports the default selected property of an option 在 Safari 4 中已修复。 */