UNPKG

istesequi

Version:

Lightweight and intuitive javascript library

763 lines (610 loc) 20.1 kB
// Umbrella JS http://umbrellajs.com/ // ----------- // Covers your basic javascript needs // Small, lightweight jQuery alternative // @author Francisco Presencia Fandos http://francisco.io/ // @inspiration http://youmightnotneedjquery.com/ // INIT // It should make sure that there's at least one element in nodes var u = function(parameter, context) { // Make it an instance of u() to avoid needing 'new' as in 'new u()' and just // use 'u().bla();'. Reference: http://stackoverflow.com/q/24019863 if (!(this instanceof u)) { // !() http://stackoverflow.com/q/8875878 return new u(parameter, context); } if (parameter instanceof u) { return parameter; } // Check if it's a css selector if (typeof parameter == "string") { // Find and store the node(s) parameter = this.select(parameter, context); } // If we're referring a specific node as in on('click', function(){ u(this) }) // or the select() function returned a single node such as in '#id' if (parameter && parameter.nodeName) { // Store the node as an array parameter = [parameter]; } // Convert to an array, since there are many 'array-like' stuff in js-land if (!Array.isArray(parameter)) { parameter = this.slice(parameter); } this.nodes = parameter; return this; }; // Map u(...).length to u(...).nodes.length u.prototype = { get length(){ return this.nodes.length; } }; // Force it to be an array AND also it clones them // http://toddmotto.com/a-comprehensive-dive-into-nodelists-arrays-converting-nodelists-and-understanding-the-dom/ u.prototype.slice = function(pseudo) { return pseudo ? [].slice.call(pseudo, 0) : []; }; // Flatten an array using u.prototype.str = function(node, i){ return function(arg){ // Call the function with the corresponding nodes if (typeof arg === 'function') { return arg.call(this, node, i); } // From an array or other 'weird' things return arg.toString(); } } // Normalize the arguments to an array of strings // Allow for several class names like "a b, c" and several parameters u.prototype.args = function(args, node, i){ // First flatten it all to a string http://stackoverflow.com/q/22920305 // If we try to slice a string bad things happen: ['n', 'a', 'm', 'e'] if (typeof args !== 'string') { args = this.slice(args).map(this.str(node, i)); } // Then convert that string to an array of not-null strings return args.toString().split(/[\s,]+/).filter(function(e){ return e.length; }); }; // Make the nodes unique. This is needed for some specific methods u.prototype.unique = function(){ return u(this.nodes.reduce(function(clean, node){ return (node && clean.indexOf(node) === -1) ? clean.concat(node) : clean; }, [])); }; // Encode the different strings https://gist.github.com/brettz9/7147458 u.prototype.uri = function(str){ return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A').replace(/%20/g, '+'); } // Parametize an object: { a: 'b', c: 'd' } => 'a=b&c=d' u.prototype.param = function(obj){ // Note: while this is ~10% slower (~3us/operation) than with a simple for(in) // I find it more legible and more 'logical' (however right now a test fails) // return Object.keys(obj).map(function(key) { // return this.uri(key) + '=' + this.uri(obj[key]); // }).join('&'); var query = ''; for(var key in obj) { query += '&' + this.uri(key) + '=' + this.uri(obj[key]); } return query.slice(1); } // This made the code faster, read "Initializing instance variables" in // https://developers.google.com/speed/articles/optimizing-javascript // Default value u.prototype.nodes = []; /** * .addClass(name1, name2, ...) * * Add a class to the matched nodes * Possible polyfill: https://github.com/eligrey/classList.js * @return this Umbrella object */ u.prototype.addClass = function(){ // Loop the combination of each node with each argument return this.eacharg(arguments, function(el, name){ // Add the class using the native method el.classList.add(name); }); }; /** * .adjacent(position, text) * * Add text in the specified position. It is used by other functions */ u.prototype.adjacent = function(position, text, data) { // Loop through all the nodes return this.each(function(node) { u(data || [""]).each(function(d, i){ // Allow for callbacks that accept some data var tx = (typeof text === 'function') ? text(d, i) : text; // http://stackoverflow.com/a/23589438 // Ref: https://developer.mozilla.org/en-US/docs/Web/API/Element.insertAdjacentHTML node.insertAdjacentHTML(position, tx); }); }); }; /** * .after(html) * * Add child after all of the current nodes * @param String html to be inserted * @return this Umbrella object */ u.prototype.after = function(text, data) { return this.adjacent('afterend', text, data); }; /** * .ajax(done, before) * * Create a POST request for whenever the matched form submits * @param function done called when response is received * @param function before called function before sending the request */ u.prototype.ajax = function(done, before) { // Attach the event submit to all of the nodes return this.on("submit", function(e) { // Stop the browser from sending the request e.preventDefault(); // Post the actual data ajax(u(this).attr("method"), u(this).attr("action"), u(this).serialize(), done, before); }); }; /** * .append(html) * * Add child the last thing inside each node * @param String html to be inserted * @return this Umbrella object */ u.prototype.append = function(html, data) { return this.adjacent('beforeend', html, data); }; /** * .attr(name, value) * * Retrieve or set the data for an attribute of the first matched node * @param String name the attribute to search * @param String value optional atribute to set * @return this|String */ // ATTR // Return the fist node attribute u.prototype.attr = function(name, value) { if (value !== undefined){ var nm = name; name = {}; name[nm] = value; } if (typeof name === 'object') { return this.each(function(node){ for(var key in name) { if (name[key] !== null){ node.setAttribute(key, name[key]); } else { node.removeAttribute(key); } } }); } return this.nodes.length ? this.first().getAttribute(name) : ""; }; /** * .before(html) * * Add child before all of the current nodes * @param String html to be inserted * @return this Umbrella object */ u.prototype.before = function(html, data) { return this.adjacent('beforebegin', html, data); }; /** * .children() * * Travel the matched elements one node down * @return this Umbrella object */ u.prototype.children = function(selector) { var self = this; return this.join(function(node){ return self.slice(node.children); }).filter(selector); }; /** * .closest() * * Find a node that matches the passed selector * @return this Umbrella object */ u.prototype.closest = function(selector) { return this.join(function(node) { // Keep going up and up on the tree. First element is also checked do { if (u(node).is(selector)) { return node; } } while (node = node.parentNode) }); }; /** * .data(name, value) * * Retrieve or set the data-* attributes of the first matched node * @param String name the data-* attribute to search * @param String value optional atribute to set * @return this|String */ // ATTR // Return the fist node attribute u.prototype.data = function(name, value) { if (typeof name === 'object') { var new_name = {}; for(var key in name) { new_name['data-' + key] = name[key]; } return this.attr(new_name); } return this.attr('data-' + name, value); }; /** * .each() * Loops through every node from the current call * it accepts a callback that will be executed on each node * The callback has two parameters, the node and the index */ u.prototype.each = function(callback) { // Loop through all the nodes this.nodes.forEach(function(node, i){ // Perform the callback for this node // By doing callback.call we allow "this" to be the context for // the callback (see http://stackoverflow.com/q/4065353 precisely) callback.call(this, node, i); }, this); return this; }; /** * .eacharg() * Loops through the combination of every node and every argument * it accepts a callback that will be executed on each combination * The callback has two parameters, the node and the index */ u.prototype.eacharg = function(args, callback) { return this.each(function(node, i){ this.args(args, node, i).forEach(function(arg){ // Perform the callback for this node // By doing callback.call we allow "this" to be the context for // the callback (see http://stackoverflow.com/q/4065353 precisely) callback.call(this, node, arg); }); }); }; // .filter(selector) // Delete all of the nodes that don't pass the selector u.prototype.filter = function(selector){ // The default function if it's a css selector // Cannot change name to 'selector' since it'd mess with it inside this fn var callback = function(node){ // Make it compatible with some other browsers node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector; // Check if it's the same element (or any element if no selector was passed) return node.matches(selector || "*"); } // filter() receives a function as in .filter(e => u(e).children().length) if (typeof selector == 'function') callback = selector; // filter() receives an instance of Umbrella as in .filter(u('a')) if (selector instanceof u) { callback = function(node){ return (selector.nodes).indexOf(node) !== -1; }; } // Just a native filtering function for ultra-speed return u(this.nodes.filter(callback)); }; /** * Find all the nodes children of the current ones matched by a selector */ u.prototype.find = function(selector) { return this.join(function(node){ return u(selector || "*", node).nodes; }); }; /** * Get the first of the nodes * @return htmlnode the first html node in the matched nodes */ u.prototype.first = function() { return this.nodes[0] || false; }; /** * ajax(url, data, success, error, before); * * Perform a POST request to the given url * @param String method the method to send the data, defaults to GET * @param String url the place to send the request * @param String data the ready to send string of data * @param function success optional callback if everything goes right * @param function error optional callback if anything goes south * @param function before optional previous callback */ function ajax(method, url, data, done, before) { // To avoid repeating it done = done || Function; // Create and send the actual request var request = new XMLHttpRequest; // An error is just an error // This uses a little hack of passing an array to u() so it handles it as // an array of nodes, hence we can use 'on'. However a single element wouldn't // work since it a) doesn't have nodeName and b) it will be sliced, failing u([request]).on('error timeout abort', function(){ done(new Error, null, request); }).on('load', function() { // Also an error if it doesn't start by 2 or 3... // This is valid as there's no code 2x nor 2, nor 3x nor 3, only 2xx and 3xx var err = !/^(2|3)/.test(request.status) ? new Error(request.status) : null; // Attempt to parse the body into JSON var body = parseJson(request.response) || request.response; return done(err, body, request); }); // Create a request of type POST to the URL and ASYNC request.open(method || 'GET', url); request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); // Load the callback before sending the data if (before) before(request); request.send(typeof data == 'string' ? data : u().param(data)); return request; } /** * parseJson(json) * * Parse JSON without throwing an error * @param String json the string to check * @return object from the json or false */ function parseJson(jsonString){ try { var o = JSON.parse(jsonString); // Handle non-exception-throwing cases: // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking // so we must check for that, too. if (o && typeof o === "object") { return o; } } catch (e) {} return false; } /** * .hasClass(name) * * Find out whether the matched elements have a class or not * @param String name the class name we want to find * @return boolean wether the nodes have the class or not */ u.prototype.hasClass = function(names) { names = this.args(arguments); // Attempt to find a node that passes the conditions return this.nodes.some(function(node){ // Check if the current node has all of the classes return names.every(function(name){ // Check whether return node.classList.contains(name) }); }); }; /** * .html(text) * * Set or retrieve the html from the matched node(s) * @param text optional some text to set as html * @return this|html Umbrella object */ u.prototype.html = function(text) { // Needs to check undefined as it might be "" if (text === undefined) { return this.first().innerHTML || ""; } // If we're attempting to set some text // Loop through all the nodes return this.each(function(node) { // Set the inner html to the node node.innerHTML = text; }); }; // .is(selector) // Check whether any of the nodes matches the selector u.prototype.is = function(selector){ return this.filter(selector).nodes.length > 0; }; /** * Merge all of the nodes that the callback returns */ u.prototype.join = function(callback) { return u(this.nodes.reduce(function(newNodes, node, i){ return newNodes.concat(callback(node, i)); }, [])).unique(); }; /** * Get the last of the nodes * @return htmlnode the last html node in the matched nodes */ u.prototype.last = function() { return this.nodes[this.nodes.length-1] || false; }; // .not(elems) // Delete all of the nodes that equals filter u.prototype.not = function(filter){ return this.filter(function(node){ return !u(node).is(filter || true); }); }; /** * .on(event, callback) * * Attach the callback to the event listener for each node * @param String event(s) the type of event ('click', 'submit', etc) * @param function callback function called when the event triggers * @return this Umbrella object */ u.prototype.on = function(events, callback) { return this.each(function(node){ this.args(events).forEach(function(event){ node.addEventListener(event, callback); }); }); }; /** * .parent() * * Travel the matched elements one node up * @return this Umbrella object */ u.prototype.parent = function(selector) { return this.join(function(node){ return node.parentNode; }).filter(selector); }; /** * .prepend(html) * * Add child the first thing inside each node * @param String html to be inserted * @return this Umbrella object */ u.prototype.prepend = function(html, data) { return this.adjacent('afterbegin', html, data); }; /** * .remove() * * Delete the matched nodes from the html tree */ u.prototype.remove = function() { // Loop through all the nodes return this.each(function(node) { // Perform the removal node.parentNode.removeChild(node); }); }; /** * .removeClass(name) * * Removes a class from all of the matched nodes * @param String name the class name we want to remove * @return this Umbrella object */ u.prototype.removeClass = function() { // Loop the combination of each node with each argument return this.eacharg(arguments, function(el, name){ // Remove the class using the native method el.classList.remove(name); }); }; // Select the adecuate part from the context u.prototype.select = function(parameter, context) { if (context) { return this.select.byCss(parameter, context); } for (var key in this.selectors) { // Reusing it to save space context = key.split('/'); if ((new RegExp(context[1], context[2])).test(parameter)) { return this.selectors[key](parameter); } } return this.select.byCss(parameter); }; // Select some elements using a css Selector u.prototype.select.byCss = function(parameter, context) { return (context || document).querySelectorAll(parameter); }; // Allow for adding/removing regexes and parsing functions // It stores a regex: function pair to process the parameter and context u.prototype.selectors = {}; // Find some html nodes using an Id u.prototype.selectors[/^\.[\w\-]+$/] = function(param) { return document.getElementsByClassName(param.substring(1)); }; //The tag nodes u.prototype.selectors[/^\w+$/] = document.getElementsByTagName.bind(document); // Find some html nodes using an Id u.prototype.selectors[/^\#[\w\-]+$/] = function(param){ return document.getElementById(param.substring(1)); }; // Create a new element for the DOM u.prototype.selectors[/^\</] = function(param){ return u(document.createElement('div')).html(param).children().nodes; }; /** * .serialize() * * Convert al html form elements into an object * The <input> and <button> without type will be parsed as default * NOTE: select-multiple for <select> is disabled on purpose * Source: http://stackoverflow.com/q/11661187 * @return string from the form's data */ u.prototype.serialize = function() { // Store the class in a variable for manipulation return this.param(this.slice(this.first().elements).reduce(function(obj, el) { // We only want to match elements with names, but not files if (el.name && el.type !== 'file' // Ignore the checkboxes that are not checked && (!/(checkbox|radio)/.test(el.type) || el.checked)) { // Add the element to the object obj[el.name] = el.value; } return obj; }, {})); }; /** * .siblings() * * Travel the matched elements at the same level * @return this Umbrella object */ u.prototype.siblings = function(selector) { return this.parent().children(selector).not(this); }; /** * .toggleClass('name1, name2, nameN' ...[, addOrRemove]) * * Toggles classes on the matched nodes * Possible polyfill: https://github.com/eligrey/classList.js * @return this Umbrella object */ u.prototype.toggleClass = function(classes, addOrRemove){ //check if addOrRemove was passed as a boolean if (!!addOrRemove === addOrRemove) { // return the corresponding Umbrella method return this[addOrRemove ? 'addClass' : 'removeClass'](classes); } // Loop through all the nodes and classes combinations return this.eacharg(classes, function(el, name){ el.classList.toggle(name); }); }; /** * .trigger(name) * ---------- * Call an event manually on all the nodes * @param event: the event or event name to call * @return u: an instance of umbrella */ u.prototype.trigger = function(event) { // Allow the event to bubble up and to be cancelable (default) var opts = { bubbles: true, cancelable: true }; try { // Accept different types of event names or an event itself event = (typeof event == 'string') ? new Event(event, opts) : event; } catch(e) { var name = event; event = document.createEvent('Event'); event.initEvent(name, opts.bubbles, opts.cancelable); } // Loop all of the nodes return this.each(function(node){ // Actually trigger the event node.dispatchEvent(event); }); };