UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

293 lines (269 loc) 10.3 kB
// # can/util/attr.js // Central location for attribute changing to occur, used to trigger an // `attributes` event on elements. This enables the user to do (jQuery example): `$(el).bind("attributes", function(ev) { ... })` where `ev` contains `attributeName` and `oldValue`. steal("can/util/can.js", function (can) { var namespaces = { 'xlink': 'http://www.w3.org/1999/xlink' }; // Acts as a polyfill for setImmediate which only works in IE 10+. Needed to make // the triggering of `attributes` event async. var setImmediate = can.global.setImmediate || function (cb) { return setTimeout(cb, 0); }, // this is a hack to deal with a problem with can-simple-dom formElements = {"input": true, "textarea": true, "select": true}, hasProperty = function(el,attrName){ return (attrName in el) || (can.document && formElements[el.nodeName.toLowerCase()]); }, attr = { // This property lets us know if the browser supports mutation observers. // If they are supported then that will be setup in can/util/jquery and those native events will be used to inform observers of attribute changes. // Otherwise this module handles triggering an `attributes` event on the element. MutationObserver: can.global.MutationObserver || can.global.WebKitMutationObserver || can.global.MozMutationObserver, /** * @property {Object.<String,(String|Boolean|function)>} can.view.attr.map * @parent can.view.elements * @hide * * * A mapping of * special attributes to their JS property. For example: * * "class" : "className" * * means get or set `element.className`. And: * * "checked" : true * * means set `element.checked = true`. * * * If the attribute name is not found, it's assumed to use * `element.getAttribute` and `element.setAttribute`. */ map: { "class": function(el, val) { val = val || ''; if(el.namespaceURI === 'http://www.w3.org/2000/svg') { el.setAttribute('class', val); } else { el.className = val; } return val; }, "value": "value", "innertext": "innerText", "innerhtml": "innerHTML", "textcontent": "textContent", "for": "htmlFor", "checked": true, "disabled": true, "readonly": function (el, val) { el.readOnly = val || typeof val === 'string' ? true : false; return val; }, "required": true, // For the `src` attribute we are using a setter function to prevent values such as an empty string or null from being set. // An `img` tag attempts to fetch the `src` when it is set, so we need to prevent that from happening by removing the attribute instead. src: function (el, val) { if (val == null || val === "") { el.removeAttribute("src"); return null; } else { el.setAttribute("src", val); return val; } }, style: (function () { var el = can.global.document && document.createElement('div'); if ( el && el.style && ("cssText" in el.style) ) { return function (el, val) { return el.style.cssText = (val || ""); }; } else { return function (el, val) { return el.setAttribute("style", val); }; } })() }, // These are elements whos default value we should set. defaultValue: ["input", "textarea"], setAttrOrProp: function(el, attrName, val){ attrName = attrName.toLowerCase(); var prop = attr.map[attrName]; if(prop === true && !val) { this.remove(el, attrName); } else { this.set(el, attrName, val); } }, setSelectValue: function(el, val) { // jshint eqeqeq: false if(val != null) { var options = el.getElementsByTagName('option'); for(var i = 0; i < options.length; i++) { if(val == options[i].value) { options[i].selected = true; return; } } } el.selectedIndex = -1; }, // ## attr.set // Set the value an attribute on an element. set: function (el, attrName, val) { var usingMutationObserver = can.isDOM(el) && attr.MutationObserver; attrName = attrName.toLowerCase(); var oldValue; // In order to later trigger an event we need to compare the new value to the old value, // so here we go ahead and retrieve the old value for browsers that don't have native MutationObservers. if (!usingMutationObserver) { oldValue = attr.get(el, attrName); } var prop = attr.map[attrName], newValue; // Using the property of `attr.map`, go through and check if the property is a function, and if so call it. // Then check if the property is `true`, and if so set the value to `true`, also making sure // to set `defaultChecked` to `true` for elements of `attr.defaultValue`. We always set the value to true // because for these boolean properties, setting them to false would be the same as removing the attribute. // // For all other attributes use `setAttribute` to set the new value. if (typeof prop === "function") { newValue = prop(el, val); } else if (prop === true && hasProperty(el, attrName)) { newValue = el[attrName] = true; if (attrName === "checked" && el.type === "radio") { if (can.inArray((el.nodeName+"").toLowerCase(), attr.defaultValue) >= 0) { el.defaultChecked = true; } } } else if (typeof prop === "string" && hasProperty(el, prop)) { newValue = val; // https://github.com/canjs/canjs/issues/356 // But still needs to be set for <option>fields if (el[prop] !== val || el.nodeName.toUpperCase() === 'OPTION') { el[prop] = val; } if (prop === "value" && can.inArray((el.nodeName+"").toLowerCase(), attr.defaultValue) >= 0) { el.defaultValue = val; } } else { attr.setAttribute(el, attrName, val); } // Now that the value has been set, for browsers without MutationObservers, check to see that value has changed and if so trigger the "attributes" event on the element. if (!usingMutationObserver && newValue !== oldValue) { attr.trigger(el, attrName, oldValue); } }, setAttribute: (function(){ var doc = can.global.document; if(doc && document.createAttribute) { try { doc.createAttribute("{}"); } catch(e) { var invalidNodes = {}, attributeDummy = document.createElement('div'); return function(el, attrName, val){ var first = attrName.charAt(0), cachedNode, node, attr; if((first === "{" || first === "(" || first === "*") && el.setAttributeNode) { cachedNode = invalidNodes[attrName]; if(!cachedNode) { attributeDummy.innerHTML = '<div ' + attrName + '=""></div>'; cachedNode = invalidNodes[attrName] = attributeDummy.childNodes[0].attributes[0]; } node = cachedNode.cloneNode(); node.value = val; el.setAttributeNode(node); } else { attr = attrName.split(':'); if(attr.length !== 1) { el.setAttributeNS(namespaces[attr[0]], attrName, val); } else { el.setAttribute(attrName, val); } } }; } } return function(el, attrName, val){ el.setAttribute(attrName, val); }; })(), // ## attr.trigger // Used to trigger an "attributes" event on an element. Checks to make sure that someone is listening for the event and then queues a function to be called asynchronously using `setImmediate. trigger: function (el, attrName, oldValue) { if (can.data(can.$(el), "canHasAttributesBindings")) { attrName = attrName.toLowerCase(); return setImmediate(function () { can.trigger(el, { type: "attributes", attributeName: attrName, target: el, oldValue: oldValue, bubbles: false }, []); }); } }, // ## attr.get // Gets the value of an attribute. First checks to see if the property is a string on `attr.map` and if so returns the value from the element's property. Otherwise uses `getAttribute` to retrieve the value. get: function (el, attrName) { attrName = attrName.toLowerCase(); var prop = attr.map[attrName]; if(typeof prop === "string" && hasProperty(el, prop) ) { return el[prop]; } else if(prop === true && hasProperty(el, attrName) ) { return el[attrName]; } return el.getAttribute(attrName); }, // ## attr.remove // Removes an attribute from an element. Works by using the `attr.map` to see if the attribute is a special type of property. If the property is a function then the fuction is called with `undefined` as the value. If the property is `true` then the attribute is set to false. If the property is a string then the attribute is set to an empty string. Otherwise `removeAttribute` is used. // // If the attribute previously had a value and the browser doesn't support MutationObservers we then trigger an "attributes" event. remove: function (el, attrName) { attrName = attrName.toLowerCase(); var oldValue; if (!attr.MutationObserver) { oldValue = attr.get(el, attrName); } var setter = attr.map[attrName]; if (typeof setter === "function") { setter(el, undefined); } if (setter === true && hasProperty(el, attrName) ) { el[attrName] = false; } else if (typeof setter === "string" && hasProperty(el, setter) ) { el[setter] = ""; } else { el.removeAttribute(attrName); } if (!attr.MutationObserver && oldValue != null) { attr.trigger(el, attrName, oldValue); } }, // ## attr.has // Checks if an element contains an attribute. // For browsers that support `hasAttribute`, creates a function that calls hasAttribute, otherwise creates a function that uses `getAttribute` to check that the attribute is not null. has: (function () { var el = can.global.document && document.createElement('div'); if (el && el.hasAttribute) { return function (el, name) { return el.hasAttribute(name); }; } else { return function (el, name) { return el.getAttribute(name) !== null; }; } })() }; return attr; });