UNPKG

cocholate

Version:

Small and fast DOM manipulation library.

1,129 lines (761 loc) 75.5 kB
# cocholate > "Why cocholate? Because it goes well with [vanilla](http://vanilla-js.com)." -- cocholate's PR. cocholate is a small library for DOM manipulation. It's meant to be small, easily understandable and fast. ## Current status of the project The current version of cocholate, v4.0.0, is considered to be *stable* and *complete*. [Suggestions](https://github.com/fpereiro/cocholate/issues) and [patches](https://github.com/fpereiro/cocholate/pulls) are welcome. Besides bug fixes, there are no future changes planned. cocholate is part of the [ustack](https://github.com/fpereiro/ustack), a set of libraries to build web applications which aims to be fully understandable by those who use it. ## Installation The dependencies of cocholate are two: - [dale](https://github.com/fpereiro/dale) - [teishi](https://github.com/fpereiro/teishi) cocholate is written in Javascript. You can use it in the browser by sourcing the dependencies and the main file: ```html <script src="dale.js"></script> <script src="teishi.js"></script> <script src="cocholate.js"></script> ``` Or you can use these links to the latest version - courtesy of [jsDelivr](https://jsdelivr.com). ```html <script src="https://cdn.jsdelivr.net/gh/fpereiro/dale@3199cebc19ec639abf242fd8788481b65c7dc3a3/dale.js"></script> <script src="https://cdn.jsdelivr.net/gh/fpereiro/teishi@31a9cf552dbaee79fb1c2b7d12c6fad20f987983/teishi.js"></script> <script src="https://cdn.jsdelivr.net/gh/fpereiro/cocholate@47a37cabfc0684091d6ff1d01f4c300f24ed11c1/cocholate.js"></script> ``` cocholate is exclusively a client-side library. Still, you can find it in npm: `npm install cocholate` Browser compatibility has been tested in the following browsers: - Google Chrome 15 and above. - Mozilla Firefox 3 and above. - Safari 4 and above. - Internet Explorer 6 and above. - Microsoft Edge 14 and above. - Opera 10.6 and above. - Yandex 14.12 and above. The author wishes to thank [Browserstack](https://browserstack.com) for providing tools to test cross-browser compatibility. ## Loading cocholate As soon as you include cocholate, it will be available on `window.c`. ```javascript var c = window.c; ``` A couple notes regarding [polyfills](https://en.wikipedia.org/wiki/Polyfill_(programming)): - If cocholate detects that the DOM method `insertAdjacentHTML` is not defined, cocholate will set it (this will [only happen](https://caniuse.com/#feat=insertadjacenthtml) in Firefox 7 and below and Safari 3 and below). - Because cocholate uses [teishi](https://github.com/fpereiro/teishi), the `indexOf` method for arrays will also be set (this will happen only in Firefox 1, Edge 12 and below and Internet Explorer 8 and below). ## Selectors `c` is the main function of the library. It takes a `selector` and an optional `fun`. Let's go with the simplest case: ```javascript // This code will return an array with all the divs in the document. c ('div'); ``` ```javascript // This code will return an array with all the elements that have the class .nav. c ('.nav'); ``` Whenever you pass a string as a `selector` and no other arguments, cocholate simply uses the native [document.querySelectorAll](http://www.w3schools.com/jsref/met_document_queryselectorall.asp), with the sole difference that returns an array instead of a [NodeList](http://www.w3schools.com/jsref/met_document_queryselectorall.asp). A very important exception is when you pass a string selector that targets an id, such as `#hola` or `div#hola`, you won't get an array - instead, you'll get either the element itself, or `undefined`: ```javascript // This code will return the div with id `hola` c ('#hola'); // This code will do the same thing. c ('#hola'); // This code, however, won't do the same thing, though it means the same thing. c ('body #hola'); // This code will return an array, since it targets the children of an element. c ('#hola p'); ``` If you invoke `c` with the string `'body'` as the selector, you will receive only the `body` itself as the result, instead of an array containing the `body`. ```javascript c ('body') === document.body // this line will be true ``` Note: in old browsers that do not support `querySelectorAll` (Firefox 3 and below, Internet Explorer 7 and below), cocholate provides a limited variety of selectors, with the following shapes: `TAG`, `#ID`, `.CLASS`, `TAG#ID` and `TAG.CLASS`. In these old browsers, if you use a selector that does not conform to these specific forms, cocholate will print an error and return `false`; in particular, the following characters are forbidden in selectors: `,>[]`. If instead of searching from all elements you want to search within a specific element, instead of a string selector you can use an object with the form `{selector: SELECTOR, from: FROM}`, where `SELECTOR` is the string selector and `FROM` is an DOM element. For example: ```javascript // This will return all divs with class `hello` from the body c ({selector: 'div', from: c ('body')}); // This will return all paragraphs with class `hello` from a div with id `hello` c ({selector: 'div', from: document.getElementById ('hello')}); // This is equivalent to the last thing we did c ({selector: 'div', from: c ('div#hello')}); ``` If you want to use the logical operations `and`, `not` and `or`, you can do so by using a `selector` that's an array where the first element is either `':and'`, `':or'` or `':not'`. ```javascript // This code will return an array with all the elements that are `div` or `p` c ([':or', 'div', 'p']); // This code will return an empty array (because there are no elements that can be simultaneously a `div` and a `p`). c ([':and', 'div', 'p']); // This code will return an array with all the elements that are neither `div` or `p`. c ([':not', 'div', 'p']); ``` You can also nest the selectors to an arbitrary degree, as long as the first element of each nested array selector is one of `':and'`, `':or'` or `':not'`: ```javascript // This code will return an array with all the elements that are `div` or `p` and are contained inside `body`. c ([':and', 'body *', [':or', 'div', 'p']]); ``` A subtle point: when you pass multiple elements to a `':not'` selector, it is actually equivalent to using the `':or'` selector. For example, `[':not', 'div', 'p']` is equivalent to writing `[':not', [':or', 'div', 'p']]`. The reason for this disambiguation is that `and` and `or` are operations on two operands, where `not` is an unary operation. The choice of `or` instead of `and` reflects what I believe is the most intuitive and common usage of `not` for multiple operands. ```javascript // These two calls will return the same result. c ([':not', 'div', 'p']); c ([':not', [':or', 'div', 'p']]); ``` If you want to perform an operation on a certain DOM element, you can directly pass it to `c`. ```javascript // These two calls are equivalent. c ('#hello'); c (document.getElementById ('hello')); ``` Note that in this case `c` will return a single result, instead of an array of results, because the DOM element is only one. ## `fun` Besides returning an array of DOM elements, we will want to do some operations on them. To do this, we can pass a second argument to `c`, which is a function that will be executed for every element that matched the selector. The results will be collected on an array that's then returned. For example, the following call will return an array with the ids of all the `divs` in the document. ```javascript c ('div', function (e) { return e.getAttribute ('id'); }); ``` All the following DOM functions are implemented as underlying calls to `c`, passing its specific logic as the second argument to `c`. ## DOM functions All the functions presented in this section take a `selector` as its first element and execute its logic for each of the elements matching the selector. ### `c.empty` `c.empty` [removes](https://www.tutorialspoint.com/prototype/prototype_element_remove.htm) all the DOM elements within the elements matched by the selector. In other words, it completely gets rid of all the DOM elements nested inside of the matching elements. This function has no meaningful return value. If an invalid selector was passed to this function, an error will be printed. ### `c.fill` `c.fill` takes `html` (an HTML string) as its second argument and then fills it with the provided HTML string. This function has no meaningful return value. If an invalid selector was passed to this function, an error will be printed. ### `c.place` `c.place` takes `where` as its second argument and `html` as its third. `where` can be one of `'beforeBegin'`, `'afterBegin'`, `'beforeEnd'`, `'afterEnd'`, and `html` is an HTML string. This function has no meaningful return value. Its functionality is based on that of [insertAdjacentHTML](https://msdn.microsoft.com/en-us/library/ms536452(v=vs.85).aspx). To explain what this function does, an example serves best. If you start with the following HTML: ```html <div id="vamo"> </div> ``` If you would execute the following function call: ```javascript c.place ('#vamo', 'beforeBegin', '<p>beforeBegin</p>'); ``` You would end up with the following HTML. ```html <p>beforeBegin</p> <div id="vamo"> </div> ``` If you then do the following three calls: ```javascript c.place ('#vamo', 'afterBegin', '<p>beforeBegin</p>'); c.place ('#vamo', 'beforeEnd', '<p>beforeEnd</p>'); c.place ('#vamo', 'afterEnd', '<p>afterEnd</p>'); ``` You will get: ```html <p>beforeBegin</p> <div id="vamo"> <p>afterBegin</p> <p>beforeEnd</p> </div> <p>afterEnd</p> ``` ### `c.get` `c.get` is useful for fetching attributes from elements. It takes `attributes` as its second argument (which can be `undefined`, a string or an array of strings, each of them representing an attribute name) and an optional boolean third parameter `css` which marks whether you want to get CSS properties instead of DOM ones. For each of the matching elements, this function will return an object where the key is the attribute name and the corresponding value is the attribute value. All these objects are wrapped in an array (with the sole exception of a selector that targets an id). For example, if you have the following HTML: ```html <p id="a" class="red"></p> <p id="b" class="blue"></p> <p id="c" class="green"></p> ``` And you run this code: ```javascript c.get ('p', 'class'); ``` You'll get an array with three objects: `[{class: 'red'}, {class: 'blue'}, {class: 'green'}]`. If you run this code: ```javascript c.get ('p', ['id', 'class']); ``` You'll get an array with three objects but two properties each: `[{id: 'a', class: 'red'}, {id: 'b', class: 'blue'}, {id: 'c', class: 'green'}]`. As with `c`, if you pass a selector that targets the id of an element, you will get the attributes themselves without them being wrapped in an array: ```javascript c.get ('#a', 'class'); // will return {class: 'red'} c.get ('p#a', 'class'); // will also return {class: 'red'} ``` Using the `css` attribute, you can obtain the CSS properties of an element. Consider this example: ```html <p style="color: red;"></p> <p style="color: blue;"></p> <p style="color: green;"></p> ``` ```javascript c.get ('p', 'color', true); ``` If you run this code on the above HTML, you will obtain an array with three objects: `[{color: 'red'}, {color: 'blue'}, {color: 'green'}]`. Finally, either with normal attributes or CSS ones, if the attribute is not present, you will get `null` as its value. For example: ```javascript c.get ('p', 'name'); // will return `[{name: null}]` c.get ('p', 'height', true); // will return `[{height: null}]` ``` If `attributes` is `undefined`, *all* the attributes will be returned, except those with falsy values (like `null`, `''`, `false`, 0 and `false`). If you want to bring all CSS attributes, you can explicitly pass `undefined` as a second argument; note that this will bring only the inline CSS attributes, and not the computed CSS values for the element. ### `c.set` This function is similar to `c.get`, except that it *sets* the attributes instead of getting them. This function has no meaningful return value. This function takes a selector as first argument, and as second argument an object with all the properties you wish to set. An optional `css` flag is the third argument, in order to set inline CSS properties. If you have the following HTML: ```html <p> </p> ``` ```javascript c.set ('p', {class: 'someclass'}); ``` The HTML will look like this: ```html <p class="someclass"> </p> ``` To remove an attribute, just pass `null` as the attribute value. If you execute this code: ```javascript c.set ('body p', {class: null}); ``` The HTML will go back to its original state: ```html <p> </p> ``` If you pass a truthy third argument, you'll set/unset CSS properties instead. ```javascript c.set ('body p', {color: 'red'}, true); ``` Now the HTML will look like this: ```html <p style="color: red;"> </p> ``` To remove the style property, you can also use `null` as a value: ```javascript c.set ('body p', {color: null}, true); ``` Now the HTML will look like this: ```html <p style=""> </p> ``` By default, if the element whose attribute is being modified has an `onchange` event handler, the `onchange` event will be automatically triggered. For example: ```html <input id="hello"> <script> c ('#hello').onchange = function () { alert (this.value); } c.set ('#hello', {value: 2}); </script> ``` Because of the event handler that we assigned to `#hello`, we will see an alert with the value `2`. This also is the case with CSS events. For example, this call to `c.set` will also trigger the `onchange` event: ```html <input id="hello"> <script> c ('#hello').onchange = function () { alert (this.style.color); } c.set ('#hello', {color: 'lime'}, true); </script> ``` If you want to override this behavior, you can simply pass a truthy fourth argument: ```javascript // for normal attributes c.set ('#hello', {value: 2}, false, true); // for CSS attributes c.set ('#hello', {color: 'lime'}, true, true); ``` ### `c.fire` This function creates an event and triggers it on the specified elements. It takes an `eventType` as its second argument, a string that determines which argument is fired (for example, `'click'`). This function has no meaningful return value. ```javascript c.fire ('#button', 'click'); ``` `c.fire` is useful for test scripts that simulate user interactions. ## Non-DOM functions Besides the six DOM functions, there are five more for a few things that are convenient to have around. ### `c.ready` A function that gets executed when the HTML page and all its resources (including stylesheets and scripts) have finished loading. Takes a single argument, `fun`, containing the code to be executed when this event happens. This function is handy to prevent executing your application code before all scripts are loaded. This will happen automatically on most browsers if you place all your scripts at the bottom of the body - the exception is Internet Explorer 8 and below, which seem to run the scripts in parallel. This function is also handy if you want to wait for all stylesheets to load before executing your script. ### `c.cookie` Quick & dirty cookie parsing. Takes an optional argument, `cookie`, a cookie string that will be parsed. If you don't pass any arguments, this function will read the cookie at `document.cookie` instead. This function returns an object with keys/values, each of them pertaining to a property of the cookie. If you pass `false` as the argument, `c.cookie` will delete all the cookies that are accessible to javascript - that is, those that don't have the [HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) directive/attribute. ### `c.ajax` This function can make ajax calls and provides a few conveniences. It takes five arguments: - `method`: a string. Defaults to `'GET'` if you pass a falsy argument. - `path`: a string with the target path. - `headers`: an object where every value is a string. Defaults to `{}` if you pass a falsy argument. - `body`: the body of the request. Defaults to `''` if you pass a falsy argument. - `callback`: a function to be executed after the request is completed. Defaults to an empty function if you pass a falsy argument. The conveniences provided are: - You can directly pass a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object, useful for `multipart/form-data` requests. - If you pass an array or object as `body`, the `content-type` header will be automatically set to `application/json` and the body will be stringified. - All ajax requests done through this function are asynchronous. - The function will synchronously return an object of the form `{headers: ..., body: ..., xhr: <the request object>}` (corresponding to the request data). - If the response has a code 200 or 304, the callback will receive `null` as its first argument and the following object as the second argument: `{headers: {...}, body: ..., xhr: <the request object>}`. If the `Content-Type` response header is `application/json`, the `body` will be parsed - if the `body` turns out to be invalid JSON, its value will be `false`. - If the code is not 200 or 304, the request object will be received as the first argument. The request object contains all the relevant information, including payloads and errors. ### `c.loadScript` This function requests a script and places it on the DOM, at the bottom of the body. It takes two arguments: `src`, the path to the javascript file; and an optional `callback` that is executed after the script is fetched. If `callback` is not passed, it will default to an empty function. `c.loadScript` uses `c.ajax` to retrieve the script asynchronously and will return the result of its invocation to `c.ajax` - which will be `false` if `src` is invalid, and a request object otherwise. If the script is successfully fetched, `callback` will receive two arguments (`null` and the request object); in case of error, it will receive the request object as its first argument. ### `c.test` This function allows to define and execute tests. It's meant as an ultra lightweight yet effective test runner. This function takes two arguments: `tests`, which is an array, and `callback`, which is an optional function to be executed when the test suite finishes running. Each of the elements contained by `tests` should also be an array. The three possible forms for each `test` array is: - `[TAG, ACTION, CHECK]` - `[TAG, CHECK]` - `[]` (this is a [no-op](https://en.wikipedia.org/wiki/NOP_(code)), useful for running a test only if a condition is met) `TAG` is a string which prints the name of the test being performed. `CHECK` is a synchronous function that performs a check; if the check is successful, the should return `true` - this will make `c.test` throw an error (unless you specify another behavior in `callback`). Any other value returned by `CHECK` (even `undefined`) will signify an error - in fact, the idea is that you return an error message whenever one of your checks fails. `ACTION` is a potentially asynchronous function that performs an action. It will be executed before `CHECK`. If this function returns a value other than `undefined`, it will be considered synchronous and `CHECK` will be executed immediately afterwards. If you however wish to perform an async operation, you can do so and not return any value. When the async operation is done, use the `next` function passed as the first argument to `ACTION`, which is the callback. If you wish to wait `n` milliseconds before `CHECK` gets called, pass a non-negative integer to `next` - this is equivalent as writing `setTimeout (next, <milliseconds>)`. If your desired wait time after an `ACTION` should be *at most* a certain number of milliseconds, you can pass a second argument to `next`, which will signify that the `CHECK` function should be executed every n milliseconds up until the total wait time (that you specified in the first argument) is elapsed. For example, if you invoke `next (1000, 10);` after executing an `ACTION`, the `CHECK` function will be run every 10 milliseconds, for up to a second, until either the `CHECK` function succeeds or the time runs up. This is very useful since most of the time, for async operations, you don't know exactly how long they will take. This, however, requires that your `CHECK` function should be able to be run multiple times without detriment to the state of the test suite. `c.test` will execute all tests in sequence and stop at the first error. It will print the `TAG` for each test about to be executed. If you have passed a `callback`, that function will receive either an `error` as its first argument or the number of milliseconds that the successful test run took to run as its second argument. If you don't provide a `callback` function, `c.test` will print either the error or success message to the console. If `c.test` receives an invalid `tests` array, it will print an error and return `false`. Otherwise, the function will return `undefined`. Note that, whether the test suite fails or succeeds, `c.test` will return `undefined` - `false` only denotes invalid tests. ### `prod` mode cocholate's functions spend most of its running time (easily 80-90%) performing validations to their inputs. While validation is essential to shorten the debug cycle when developing, in certain cases you might want to turn it off to improve performance. This can be done by enabling `prod` mode. To do this, set `c.prod` to `true`. The cost of turning off validation is that if there's an invalid invocation somewhere, an error will be thrown. ## Source code The complete source code is contained in `cocholate.js`. It is about 360 lines long. Below is the annotated source. ```javascript /* cocholate - v4.0.0 Written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain. Please refer to readme.md to read the annotated source. */ ``` ### Setup We wrap the entire file in a self-executing anonymous function. This practice is commonly named [the javascript module pattern](http://yuiblog.com/blog/2007/06/12/module-pattern/). The purpose of it is to wrap our code in a closure and hence avoid making the local variables we define here to be available outside of this module. ```javascript (function () { ``` If we're in node.js, we print an error and return `undefined`. ```javascript if (typeof exports === 'object') return console.log ('cocholate only works in a browser!'); ``` We require [dale](http://github.com/fpereiro/dale) and [teishi](http://github.com/fpereiro/teishi). Note that, in the browser, `dale` and `teishi` will be loaded as global variables. ```javascript var dale = window.dale; var teishi = window.teishi; ``` We create an alias to `teishi.type`, the function for finding out the type of an element. We do the same for `teishi.clog`, a function for printing logs that also returns `false`. We also do the same for `teishi.inc`, a function for checking whether a given element is contained in an array. ```javascript var type = teishi.type, clog = teishi.clog, inc = teishi.inc; ``` ### Polyfill for `insertAdjacentHTML` We will define a [polyfill](https://en.wikipedia.org/wiki/Polyfill_(programming)) for `insertAdjacentHTML`, which will be necessary in old versions of Safari and Firefox. It is based on [Eli Grey's polyfill](https://gist.github.com/eligrey/1276030). We set the function only if it's not defined. The function takes two arguments, `position` and `html`. ```javascript if (! document.createElement ('_').insertAdjacentHTML) HTMLElement.prototype.insertAdjacentHTML = function (position, html) { ``` We create a container element and then we set its `innerHTML` property to the `html` we received as a string. This will create all the desired DOM nodes inside `container`. ```javascript var container = document.createElement ('div'); container.innerHTML = html; ``` We now iterate the outermost elements inside `container`. By outermost, I mean that only those elements that are direct children of `container` will be iterated - whereas elements that are inside these top-level children will not be iterated. We're, however, iterating the elements in a rather strange way. Instead of using a for loop or an equivalent functional construct, we're executing the same piece of code as long as container has one element. How can this work without setting us for an infinite loop? Let's see it in a minute. ```javascript while (container.firstChild) { ``` If `position` is `beforeBegin`, we place `container.firstChild` before the element, using the `insertBefore` method on the element's parent. Once we do this, `container.firstChild` (the first element we're positioning) will be in its desired position and not on `container` anymore. in this way, the next time the while loop runs, `container.firstChild` will be the second element we want to place - and if there's no second element, the loop will be finished. ```javascript if (position === 'beforeBegin') this.parentNode.insertBefore (container.firstChild, this); ``` If `position` is `afterBegin`, we place `container.firstChild` as the first children of the element using the `insertBefore` method on the element itself. ```javascript else if (position === 'afterBegin') this.insertBefore (container.firstChild, this.firstChild); ``` If `position` is `beforeEnd`, we merely append `container.firstChild` to the element. ```javascript else if (position === 'beforeEnd') this.appendChild (container.firstChild); ``` Finally, if `position` is `afterEnd`, we place `container.firstChild` just after the element using the `insertBefore` method on the parent. ```javascript else this.parentNode.insertBefore (container.firstChild, this.nextElementSibling) ``` There's nothing else to do, so we close the loop and the polyfill function. ```javascript } } ``` ### Core We define `c`, the main function of the library. Note we also attach it to `window.c`, so that it is globally available to other scripts. This function takes two arguments, `selector` and `fun`. `c`, besides being a function, will serve as an object that collects the other functions of the library. ```javascript var c = window.c = function (selector, fun) { ``` If `prod` mode is not enabled, we check that `fun` is a function or `undefined`. If it is neither, an error is printed and the function returns `false`. Note we pass `true` as the fourth argument to `teishi.stop`. We will do this for every invocation of `teishi.stop` and `teishi.v`, to tell teishi not to validate our validation rules. This will yield a (very small) performance improvement. ```javascript if (! c.prod && teishi.stop ('c', ['fun', fun, ['function', 'undefined'], 'oneOf'], undefined, true)) return false; ``` We create a local variable that will indicate whether the `selector` is actually a DOM node. ```javascript var selectorIsNode = selector && selector.nodeName; ``` We define a local variable `elements` that will contain all the DOM elements to which `selector` refers. If the selector is itself a DOM node, we wrap it in an array. Otherwise, the search for all matching elements is done by `c.find`, a function which we'll see below. ```javascript var elements = selectorIsNode ? [selector] : c.find (selector); ``` If `c.find` returns `false`, this means that the selector is invalid. In this case, `c.find` will have already printed an error message. We return `false`. ```javascript if (elements === false) return false; ``` If we're here, the input is valid. If `fun` was passed, we first collect all extra arguments passed to `c` into an array named `args`. This array will always exclude `selector` and `fun`. If no extra arguments were passed, `args` will be an empty array. ```javascript if (fun) { var args = dale.go (arguments, function (v) {return v}).slice (2); ``` We iterate through `elements`, the DOM elements that match `selector`. For each of them, we apply them to `fun`, with each of them as the first argument and further arguments also passed. We collect the results of these function applications into an array and set it to `elements`. This means that if `fun` is present, `elements` will contain the results of passing each of `elements` to `fun` - whereas if `fun` is absent, `elements` will contain the DOM elements themselves. ```javascript elements = dale.go (elements, function (v) { return fun.apply (undefined, [v].concat (args)); }); } ``` If the selector a DOM node, or if it is the string `'body'`, or if it is of the form `#ID` or `TAGNAME#ID`, we return the first (and only) element of `elements`. ```javascript if (selectorIsNode || selector === 'body' || (type (selector) === 'string' && selector.match (/^[a-z0-9]*#[^\s\[>,:]+$/))) return elements [0]; ``` Otherwise, we return the entire array of `elements`. There's nothing else to do, so we close the function. ```javascript return elements; } ``` `c.nodeListToArray` is a helper function that converts a [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) into a plain array containing DOM elements. Whenever the browser returns a NodeList, we convert it into a plain array - this simplifies iteration, especially in old browsers. This function takes a single `nodeList` as its argument; since we always pass it a valid NodeList, we don't validate its input. ```javascript c.nodeListToArray = function (nodeList) { ``` We define an `output` array. ```javascript var output = []; ``` We iterate `nodeList` with a plain old for loop. We cannot use `dale.go` here since Safari 5.1 and below consider `nodeList` to be of type `function`. ```javascript for (var i = 0; i < nodeList.length; i++) { ``` We push each of the elements to `output`. ```javascript output.push (nodeList [i]); } ``` We return `output` and close the function. ```javascript return output; } ``` We define `c.setop`, a helper function that performs set operations (`and`, `or` and `not`). This function takes an operation (`and|or|not`) and two arrays of DOM elements. Each of these sets are compared according to the given operation. ```javascript c.setop = function (operation, set1, set2) { ``` If the operation is `and`, we go through the first set and create a new array filtering out those elements from the first set that are not present on the second set. We return this filtered first set, which represents an intersection of both sets. ```javascript if (operation === 'and') return dale.fil (set1, undefined, function (v) { if (inc (set2, v)) return v; }); ``` We copy the first set onto a new array `output`, since we don't want to modify it. ```javascript var output = set1.slice (); ``` If the operation is `or`, we iterate the elements of the second set. Each of the elements of the second set that are not on the first set will be pushed onto output. `or` represents an union of both sets. ```javascript if (operation === 'or') { dale.go (set2, function (v) { if (! inc (output, v)) output.push (v); }); } ``` If we're here, the operation is `not`. In this case, we want to substract the second set from the first. ```javascript else { ``` If the first set is empty, we put in `output` all the elements from the document. ```javascript if (output.length === 0) output = c.nodeListToArray (document.getElementsByTagName ('*')); ``` We remove from `output` all the elements that are present in the second set. ```javascript dale.go (set2, function (v) { var index = output.indexOf (v); if (index > -1) output.splice (index, 1); }); } ``` We return `output` and close the function. ```javascript return output; } ``` `c.find` is a function that resolves a cocholate selector into a set of DOM elements. It is used by the main (`c`) function to find elements. It takes a single argument, `selector`. ```javascript c.find = function (selector) { ``` We get the type of `selector`. ```javascript var selectorType = type (selector); ``` `selector` must be either an array, a string or an object. ```javascript if (! c.prod && teishi.stop ('cocholate', [ ['selector', selector, ['array', 'string', 'object'], 'oneOf'], ``` If `selector` is an array, its first element must be a string with a colon plus one of the operations `and`, `or` and `not`. ```javascript function () {return [ [selectorType === 'array', ['first element of array selector', selector [0], [':and', ':or', ':not'], 'oneOf', teishi.test.equal]], ``` If `selector` is an object, its keys must be `selector` and `from`. `selector.selector` must be an array or string. ```javascript [selectorType === 'object', [ ['selector keys', dale.keys (selector), ['selector', 'from'], 'eachOf', teishi.test.equal], ['selector.selector', selector.selector, ['array', 'string'], 'oneOf'], ``` Now we validate `selector.from`. We expect it to be a DOM element. If the browser supports `querySelectorAll`, we check that the DOM element is valid by testing whether the `querySelectorAll` element exists for the given `selector.from`. An implementation note: we write this last validation rule as a function and not an array because Internet Explorer 8 and below throw a strange error when placing DOM elements within a teishi array rule. ```javascript function () { if (type (selector.from) !== 'object' || (document.querySelectorAll && ! selector.from.querySelectorAll)) return clog ('c.find', 'selector.from passed to cocholate must be a DOM element.'); return true; } ``` If any of the validations fail, we print an error and return `false`. ```javascript ]] ]} ], undefined, true)) return false; ``` First we'll cover the cases where `selector` is either a string or an object. ```javascript if (selectorType !== 'array') { ``` If the browser supports `querySelectorAll` (which should happen for any of the browsers we support except Firefox 3 and below and Internet Explorer 7 and below) and `selector` is a string, we merely invoke `document.querySelectorAll` on it, convert the NodeList into an array, and return it. ```javascript if (document.querySelectorAll && selectorType === 'string') return c.nodeListToArray (document.querySelectorAll (selector)); ``` If `selector` is an object, we invoke `querySelectorAll` on `selector.from` (which is a DOM element) and we use `selector.selector` as the selector. We also convert the NodeList into an array and return it. ```javascript if (document.querySelectorAll && selectorType === 'object') return c.nodeListToArray (selector.from.querySelectorAll (selector.selector)); ``` If we're here, we're still dealing with a string or object selector, but on either Firefox 3 and below or Internet Explorer 7 and below. This is where it gets fun. In this section, we'll write code to provide limited selector support to these old browsers. We define a variable `from` that will be the context for selecting DOM elements. It will be `selector.from` (if defined) or `document` (if `selector` is a string). ```javascript var from = selector.from ? selector.from : document; ``` If `selector` is an object, we reassign it to `selector.selector`. ```javascript selector = selectorType === 'string' ? selector : selector.selector; ``` We are going to provide limited support for selectors; namely, we will only support selectors of these shapes: `TAG`, `TAG#ID`, `TAG.CLASS`, `#ID`, `.CLASS`. Note that we also support `*`, since it's possible to pass a wildcard to `document.getElementsByTagName` (which means that all elements will be selected). If `selector` doesn't conform to any of these shapes, we will print an error and return `false`. We make sure to forbid the characters `,`, `>`, `[` and `]` since those have special meaning on modern DOM selectors. ```javascript if (selector !== '*' && ! selector.match (/^[a-z0-9]*(#|\.)?[^,>\[\]]+$/i)) return clog ('The selector ' + selector + ' is not supported in IE <= 7 or Firefox <= 3.'); ``` If we're here, `selector` is supported. We will now determine what's the criterium for selecting elements; if there's a `#` in the selector, it will be by `id`; if there's a `.`, it will be by `class`. If there's neither, we'll set it to `undefined` (in which case it means that we will select elements by tag). ```javascript var criterium = selector.match ('#') ? 'id' : (selector.match (/\./) ? 'class' : undefined); ``` We split `selector` by either `#` or `.`. ```javascript selector = selector.split (/#|\./); ``` We define `tag`, a variable that will indicate whether we filter our elements to belong to a certain tag name. If selector was split in two (because there's either a hashtag or a dot), we'll set `tag` to its first element; if it was not split in two (which means absence of both hashtag and dot), we'll also set `tag` to its first element. If selector has length 1 and there's a class or hashtag present, then `tag` will be `undefined` (because selector will only contain `class` or `id` information). Note that, if present, we convert `tag` to uppercase since browsers expect it to be uppercase. ```javascript var tag = (selector.length === 2 || ! criterium) ? selector [0].toUpperCase () : undefined; ``` We invoke `getElementsByTagName` on `from`; if a specific `tag` is required, we pass it as an argument to this function; otherwise, we pass a wildcard to get all the elements. Now, if `selector.from` was passed, we want only the child elements of `from` to be selected; if `selector.from` is absent, then we'll select elements from all the elements in the document. The invocation to `getElementsByTagName` returns a NodeList. We convert it to an array of DOM elements with `c.nodeListToArray`. Once we have this array of elements, we iterate them, filtering out those for which the iterating function returns `undefined`. In other words, the function we're about to define (which takes one node at a time), will determine whether the iterated element is selected. ```javascript return dale.fil (c.nodeListToArray (from.getElementsByTagName (tag || '*')), undefined, function (node) { ``` If we're selecting elements by `class` and this element's class doesn't match it, we ignore the element. Note we split `node.className` (if it exists) by whitespace into an array of classes, and make sure that the class we're looking for is one of the elements of that array. ```javascript if (criterium === 'class' && ! inc ((node.className || '').split (/\s/), teishi.last (selector))) return; ``` If we're selecting an element by `id` and the element's id doesn't match the id we're looking for, we ignore the element. ```javascript if (criterium === 'id' && node.id !== teishi.last (selector)) return; ``` If we're here, we will return the element since it matches the required criteria. ```javascript return node; ``` We close the iteration function and also this block, since there's nothing left to do. ```javascript }); } ``` If we're here, `selector` is an array. This means that `selector` contains logical operations (`and/or/not`). We will start by extracting `operation`, which is the logical operation that is the first element of the `selector` array. We'll also define `output`, an array where we'll collect the matching elements. ```javascript var operation = selector.shift (), output = []; ``` We iterate `selector`, which contains a number of selectors that could be themselves arrays, objects or strings. We will stop whenever any of these iterations returns `false`. The approach we take is to iterate the selectors and apply logical operations onto the `output` set incrementally as we iterate through the selectors. ``` dale.stop (selector, false, function (v, k) { ``` We do a recursive call to `c.find` passing it the selector we're iterating. We collect the result of the recursive call in a local variable `elements`. ```javascript var elements = c.find (v); ``` If the recursive call to `c.find` is `false`, it means the selector is invalid. We set `output` to `false` and return `false`, which will stop the iteration. ```javascript if (elements === false) return output = false; ``` If we're on the first selector and the operation is either `and` or `or`, we set `output` to `elements`. This initializes `output`, since before this operation it was empty. ```javascript if (k === 0 && operation !== ':not') output = elements; ``` If we're not on the first selector, or we're using the `not` criterium, we apply the logical operation (through `c.setop`) with `output` (the already selected elements) and `elements` (the new elements to be taken into consideration). ```javascript else output = c.setop (operation.replace (':', ''), output, elements); ``` We finish iterating the elements. ```javascript }); ``` If any of the selectors was `false`, `output` will also be `false`. If all the selectors were valid, `output` will contain the selected elements. We return it and close the function. ```javascript return output; } ``` ### DOM functions We now start with the first of our DOM functions. All the DOM functions internally use the `c` function and the other helper functions we have seen. Let's start with `c.empty`, which deletes all the child elements within the selected elements. This function takes a single argument, a `selector`. ```javascript c.empty = function (selector) { ``` We call `c` with `selector` and a function as arguments. This function will be executed for each of the elements that match `selector`. ```javascript c (selector, function (element) { ``` We merely set the element's `innerHTML` to an empty string. ```javascript element.innerHTML = ''; ``` There's nothing else to do, so we close the iteration function, the invocation to `c` and the function. Note that we don't return any values. ```javascript }); }); } ``` We now define `c.fill`, which takes `selector` and `html` as arguments. ```javascript c.fill = function (selector, html) { ``` If `html` is not a string, the function will print an error message and return `false`. ```javascript if (! c.prod && teishi.stop ('c.fill', ['html', html, 'string'], undefined, true)) return false; ``` We iterate the elements matched by `selector` (through a call to `c`) and set their `innerHTML` property to `html`. ```javascript c (selector, function (element) { element.innerHTML = html; ``` There's nothing else to do, so we close the function. Note that we don't return any values. ```javascript }); } ``` We now define `c.place`, a function that takes three arguments: `selector`, `where` and `html`. ```javascript c.place = function (selector, where, html) { ``` We make sure that `where` is one of four strings: `beforeBegin|afterBegin|beforeEnd|afterEnd`, and that `html` is a string. If either of these conditions is not fulfilled, we print an error message and return `false`. ```javascript if (! c.prod && teishi.stop ('c.place', [ ['where', where, ['beforeBegin', 'afterBegin', 'beforeEnd', 'afterEnd'], 'oneOf', teishi.test.equal], ['html', html, 'string'] ], undefined, true)) return false; ``` For each of the elements matching `selector`, we apply [insertAdjacentHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML) with `where` and `html` as its arguments. ```javascript c (selector, function (element) { element.insertAdjacentHTML (where, html); ``` There's nothing else to do, so we close the function. Note that we don't return any values. ```javascript }); } ``` We now define `c.get`, which takes three arguments: `selector`, `attributes` and `css`. The last argument is a flag (presumably boolean), that will determine whether we're referring to CSS attributes or not. ```javascript c.get = function (selector, attributes, css) { ``` If `attributes` is not string, `undefined`, nor an array, we print an error and return `false`. ```javascript if (! c.prod && teishi.stop ('c.get', ['attributes', attributes, ['string', 'array', 'undefined'], 'oneOf'], undefined, true)) return false; ``` We define an array `ignoredValues` with attribute values which we will ignore. This is only necessary for iterating style attributes in Internet Explorer 8 and element attributes in Internet Explorer 7 and below. ```javascript var ignoredValues = [null, '', false, 0, "false"]; ``` We iterate the elements matched by `selector` and apply the following function to each of them. Note that we will return an array containing the output of this function for each of the elements. ```javascript return c (selector, function (element) { ``` If `attributes` is not `undefined`, we iterate it - if `attributes` is a string, this will be equivalent to having a single attribute. We will create an object with the attributes and return it. ```javascript if (attributes !== undefined) return dale.obj (attributes, function (v) { ``` If the `css` flag is enabled, we'll access `element.style [v]` (which contains the CSS attribute). If the attribute is not present, we will consider it to be `null`. ```javascript if (css) return [v, element.style [v] || null]; ``` If the `css` flag is disabled, we will instead return the element's attribute (accessed through `getAttribute`). ```javascript else return [v, element.getAttribute (v)]; }); ``` If we're here, `attributes` is `undefined`, which means we want all the element's attributes. If `css` is falsy, we want the actual attributes (as opposed to the style attributes) of the element. We iterate `element.attributes`. Note that we start with a base object with the element's `class` or the element `className` (if `class` is absent) - this is only for the benefit of Internet Explorer 7 and below. ```javascript if (! css) return dale.obj (element.attributes, (element ['class'] || element.className) ? {'class': element ['class'] || element.className} : {}, function (v, k) { ``` If the attribute is truthy, if its `nodeName` is truthy, and its `nodeValue` is not one of the values we are ignoring, we return them both. Checking whether the attribute is truthy is only necessary in Internet Explorer 7 and below; many browsers, however, require us to check whether `nodeName` is truthy, otherwise `undefined` attributes will be returned. ```javascript if (v && v.nodeName && ! inc (ignoredValues, v.nodeValue)) return [v.nodeName, v.nodeValue]; }); ``` If we're here, we want all inline CSS attributes for the element. In all supported browsers except for Internet Explorer 8, `element.style` has a length property that we will use to iterate the style object. In Internet Explorer 8 and below, however, we're forced to iterate all the keys of the object. ```javascript return dale.obj (element.style.length ? dale.times (element.style.length, 0) : dale.keys (element.style), function (k) { ``` If `element.style.length` is supported, we simply return the corresponding key and value of the style object. ```javascript if (element.style.length) return [element.style [k], element.style [element.style [k]]]; ``` For Internet Explorer 8 and below, we return the key and value but only if the value is not one of the `ignoredValues`. ```javascript if (! inc (ignoredValues, element.style [k])) return [k, element.style [k]]; }); ``` There's nothing else to do, so we close the iterating function, the invocation to `c` and the function itself. ```javascript }); } ``` We now define `c.set`. It is similar to `c.get`, but instead of returning attributes, it sets them. It takes four arguments, `selector`, `attributes`, `css` and `notrigger` - the last two are flags. ```javascript c.set = function (selector, attributes, css, notrigger) { ``` We now validate the input. `attributes` must be an object. ```javascript if (! c.prod && teishi.stop ('c.set', [ ['attributes', attributes, 'object'], ``` Every attribute key must start with a ASCII letter, underscore or colon, and must follow with zero or more of the following: - A letter. - An underscore. - A colon. - A digit. - A period. - A dash. - Any Unicode character with a code point of 129 (`0080` in hexadecimal) or above - these include all extended ASCII characters (the top half of the set) and every non-ASCII character. This arcana was kindly provided [by this article](http://razzed.com/2009/01/30/valid-characters-in-attribute-names-in-htmlxml/). The regex below was taken from the article and modified to add the permitted Unicode characters. ```javascript [ ['attribute keys', 'start with an ASCII letter, underscore or colon, and be followed by letters, digits, underscores, colons, periods, dashes, extended ASCII characters, or any non-ASCII characters.'], dale.keys (attributes), /^[a-zA-Z_:][a-zA-Z_:0-9.\-\u0080-\uffff]*$/, 'each', teishi.test.match ], ``` The att