Optimized for minimal DOM manipulation. The most common usecase of adding a single class will use native Element.classList if available. Otherwise, it will do a single read of el.className and only write back to it once, and only if something was actually changed.
note: Don't mix-and-match approaches. For example, don't do addClass(el, 'foo', ['bar', 'baz']). If you need this, flatten it first.
Example
var addClass = require('amp-add-class');
var element = document.querySelector('#my-el');
element.outerHTML; //=> '<div>greetings</div>';
// basic adding
addClass(element, 'oh');
element.outerHTML; //=> '<div class="oh">greetings</div>';
// adding an existing class does nothing
addClass(element, 'oh');
// add multiple at once
addClass(element, 'hello', 'there');
element.outerHTML; //=> '<div class="oh hello there">greetings</div>';
// using array
addClass(element, ['foo', 'bar']);
element.outerHTML; //=> '<div class="oh hello there foo bar">greetings</div>';
// add multiple at once with space-separated string
addClass(element, 'baz boo');
element.outerHTML; //=> '<div class="oh hello there baz boo">greetings</div>';
var hasClass = require('amp-has-class');
var isString = require('amp-is-string');
var isArray = require('amp-is-array');
var trim = require('amp-trim');
var slice = Array.prototype.slice;
var cleanup = /\s{2,}/g;
var ws = /\s+/;
module.exports = function addClass(el, cls) {
if (arguments.length === 2 && isString(cls)) {
cls = trim(cls).split(ws);
} else {
cls = isArray(cls) ? cls : slice.call(arguments, 1);
}
// optimize for best, most common case
if (cls.length === 1 && el.classList) {
if (cls[0]) el.classList.add(cls[0]);
return el;
}
var toAdd = [];
var i = 0;
var l = cls.length;
var item;
var clsName = el.className;
// see if we have anything to add
for (; i < l; i++) {
item = cls[i];
if (item && !hasClass(clsName, item)) {
toAdd.push(item);
}
}
if (toAdd.length) {
el.className = trim((clsName + ' ' + toAdd.join(' ')).replace(cleanup, ' '));
}
return el;
};
var test = require('tape');
var addClass = require('./add-class');
function getEl(className) {
var div = document.createElement('div');
div.className = className;
return div;
}
test('amp-add-class', function (t) {
var el = getEl('oh');
addClass(el, 'oh');
t.equal(el.className, 'oh');
el = getEl('oh');
addClass(el, 'oh');
t.equal(el.className, 'oh', 'adding existing class should do nothing');
addClass(el, 'hello', 'there');
t.equal(el.className, 'oh hello there', 'should be able to add several');
addClass(el, 'oh', 'hello', 'there');
t.equal(el.className, 'oh hello there', 'should never add dupes');
addClass(el, undefined, null, NaN, 0, '');
t.equal(el.className, 'oh hello there', 'should be reasonably tolerant of nonsense');
el = getEl('');
addClass(el, ['oh', 'hello', 'there']);
t.equal(el.className, 'oh hello there', 'should work with arrays too');
el = getEl(' oh hello there ');
var clsName = el.className;
t.equal(clsName, addClass(el, 'hello', 'there').className, 'should not touch classNames if not modifying');
t.equal(addClass(el, 'hello', 'yo').className, 'oh hello there yo', 'should clean up className whitespacing if modifying anyway');
el = getEl('oh');
addClass(el, 'hello there');
t.equal(el.className, 'oh hello there', 'supports adding space separated chars');
el = getEl('oh');
addClass(el, '\t\r\nhello \n there ');
t.equal(el.className, 'oh hello there', 'tolerate extra spaces when adding space separated chars');
t.end();
});
amp-has-class
Returns true if the element has the class and false otherwise.
If the first argument is a string, it is assumed to be what you'd get from el.className. This allows it to be used repeatedly for a single element without reading from the DOM more than once.
Example
var hasClass = require('amp-has-class');
var el = document.querySelector('#my-div');
el.outerHTML; //=> <div class="oh hi">hello</div>
hasClass(el, 'oh'); //=> true
hasClass(el, 'no'); //=> false
// also works with the string result of `el.className`
var className = el.className;
hasClass(className, 'oh'); //=> true
hasClass(className, 'no'); //=> false
var isString = require('amp-is-string');
var whitespaceRE = /[\t\r\n\f]/g;
// note: this is jQuery's approach
module.exports = function hasClass(el, cls) {
var cName = (isString(el) ? el : el.className).replace(whitespaceRE, ' ');
return (' ' + cName + ' ').indexOf(' ' + cls + ' ') !== -1;
};
var test = require('tape');
var hasClass = require('./has-class');
function getEl(className) {
var div = document.createElement('div');
div.className = className;
return div;
}
test('amp-has-class', function (t) {
t.ok(hasClass(getEl('hi'), 'hi'));
t.ok(hasClass(getEl('hi there'), 'there'));
t.notOk(hasClass(getEl('hi'), 'something'));
t.notOk(hasClass(getEl('hi there'), 'something'));
t.notOk(hasClass(getEl('hi-there'), 'hi'));
t.notOk(hasClass(getEl('hi-there'), 'there'));
t.notOk(hasClass(getEl('hi-there'), 'i-t'));
t.notOk(hasClass(getEl('hi-there'), '-'));
var className = getEl('hi there').className;
t.ok(hasClass(className, 'hi'));
t.ok(hasClass(className, 'there'));
t.notOk(hasClass(className, 'something'));
t.ok(hasClass(getEl('\thi'), 'hi'), 'can be \t issue #27');
t.ok(hasClass(getEl('\nhi'), 'hi'), 'can be \n issue #27');
t.ok(hasClass(getEl('\rhi'), 'hi'), 'can be \r issue #27');
t.ok(hasClass(getEl('\fhello\fhi\f'), 'hi'), 'can be \f issue #27');
t.end();
});
Optimized for minimal DOM manipulation. The most common usecase of removing a single class will use native Element.classList if available. Otherwise, it will do a single read of el.className and only write back to it once, and only if something was actually changed.
note: Don't mix-and-match approaches. For example, don't do removeClass(el, 'foo', ['bar', 'baz']). If you need this, flatten it first.
Example
var removeClass = require('amp-remove-class');
var element = document.querySelector('#my-el');
element.outerHTML; //=> '<div class="oh hello there">greetings</div>';
removeClass(element, 'oh');
element.outerHTML; //=> '<div class="hello there">greetings</div>';
// remove multiple at once
removeClass(element, 'hello', 'there');
// can be done with a space-separated string
removeClass(element, 'hello there');
// can also be done by passing array
removeClass(element, ['hello', 'there']);
element.outerHTML; //=> '<div class="">greetings</div>';
var isString = require('amp-is-string');
var isArray = require('amp-is-array');
var trim = require('amp-trim');
var slice = Array.prototype.slice;
var cleanup = /\s{2,}/g;
var ws = /\s+/;
module.exports = function removeClass(el, cls) {
if (arguments.length === 2 && isString(cls)) {
cls = trim(cls).split(ws);
} else {
cls = isArray(cls) ? cls : slice.call(arguments, 1);
}
// optimize for best, most common case
if (cls.length === 1 && el.classList) {
if (cls[0]) el.classList.remove(cls[0]);
return el;
}
// store two copies
var clsName = ' ' + el.className + ' ';
var result = clsName;
var current;
var start;
for (var i = 0, l = cls.length; i < l; i++) {
current = cls[i];
start = current ? result.indexOf(' ' + current + ' ') : -1;
if (start !== -1) {
start += 1;
result = result.slice(0, start) + result.slice(start + current.length);
}
}
// only write if modified
if (clsName !== result) {
el.className = trim(result.replace(cleanup, ' '));
}
return el;
};
var test = require('tape');
var removeClass = require('./remove-class');
function getEl(className) {
var div = document.createElement('div');
div.className = className;
return div;
}
test('amp-remove-class', function (t) {
var el = getEl('oh hello there');
removeClass(el, 'oh');
t.equal(el.className, 'hello there');
removeClass(el, 'oh');
t.equal(el.className, 'hello there', 'removing non-existant class should do nothing');
removeClass(el, 'hello', 'there');
t.equal(el.className, '', 'should be able to remove several');
removeClass(el, 'oh', 'hello', 'there');
t.equal(el.className, '', 'should be ok if doing again');
// reset it
el = getEl('oh hello there');
removeClass(el, undefined, null, NaN, 0, '');
t.equal(el.className, 'oh hello there', 'should be reasonably tolerant of nonsense');
el = getEl('undefined null NaN');
removeClass(el, undefined, null, NaN, 0, '');
t.equal(el.className, 'undefined null NaN', 'should not remove classes named `undefined` etc.');
el = getEl('oh hello there');
t.equal(el.className, 'oh hello there');
removeClass(el, ['oh', 'hello', 'there']);
t.equal(el.className, '');
el = getEl(' oh hello there');
var clsName = el.className;
t.equal(clsName, removeClass(el, 'not', 'changing').className, 'should not touch classNames if not modifying');
t.equal(removeClass(el, 'hello', 'yo').className, 'oh there', 'should clean up classNames whitespacing, if modifying anyway');
el = removeClass(getEl('foobar'), 'foo', 'bar');
t.equal(el.className, 'foobar');
el = getEl('oh hello there');
t.equal(removeClass(el, 'hello there').className, 'oh', 'should support space separated string of classes');
el = getEl('oh hello there');
t.equal(removeClass(el, '\r hello \t \nthere ').className, 'oh', 'should support other weird whitespace when using a string of classes');
t.end();
});
amp-toggle-class
Toggles the existence of a class on an element. If the class exists it will be removed, if it doesn't it will be added.
If a condition argument is supplied, the truthiness of that condition is what determines whether the class should exist on the element or not. This simplifies common use case of an element needing a class if condition is true. condition can be passed as either a primitive which will eventually coerce to boolean or as a function. If you pass a function, you get current element passed as a first argument.
Example
var toggleClass = require('amp-toggle-class');
var element = document.querySelector('#my-el');
element.outerHTML; //=> '<div>greetings</div>';
// if no condition, toggles based on presence
toggleClass(element, 'oh');
element.outerHTML; //=> '<div class="oh">greetings</div>';
// running again, will remove it
toggleClass(element, 'oh');
element.outerHTML; //=> '<div class="">greetings</div>';
// toggling based on condition
// the `condition` always wins.
// Here, the class is missing,
// but condition is falsy
// so `toggleClass` does nothing.
toggleClass(element, 'oh', false);
element.outerHTML; //=> '<div class="">greetings</div>';
var isUndefined = require('amp-is-undefined');
var isFunction = require('amp-is-function');
var hasClass = require('amp-has-class');
var addClass = require('amp-add-class');
var removeClass = require('amp-remove-class');
var actions = {
add: addClass,
remove: removeClass
};
module.exports = function toggleClass(el, cls, condition) {
var action;
if (!isUndefined(condition)) {
condition = isFunction(condition) ? condition.call(null, el) : condition;
action = condition ? 'add' : 'remove';
} else {
action = hasClass(el, cls) ? 'remove' : 'add';
}
return actions[action](el, cls);
};
var test = require('tape');
var toggleClass = require('./toggle-class');
function getEl(className) {
var div = document.createElement('div');
div.className = className;
return div;
}
test('amp-toggle-class', function (t) {
var el = getEl('oh');
toggleClass(el, 'oh');
t.equal(el.className, '', 'removes if present without condition passed');
toggleClass(el, 'oh');
t.equal(el.className, 'oh', 'puts it back when called again');
el = getEl('oh');
toggleClass(el, 'oh', true);
t.equal(el.className, 'oh', 'should leave it alone if condition is boolean true and class is already present');
el = getEl('oh');
toggleClass(el, 'hi', true);
t.equal(el.className, 'oh hi', 'should add class if condition is boolean true');
el = getEl('');
toggleClass(el, 'oh', false);
t.equal(el.className, '', 'should not remove class if not present and condition is false');
el = getEl('oh');
toggleClass(el, 'oh', false);
t.equal(el.className, '', 'should remove class if condition is boolean false');
// toggling with condition as a booleans
el = getEl('oh');
toggleClass(el, 'hi', function (element) {
t.equal(el, element, 'should pass element to a condition function');
return 1 + 1 === 2;
});
t.equal(el.className, 'oh hi', 'should add class if condition is a function returning true');
el = getEl('oh');
toggleClass(el, 'oh', function (element) {
t.equal(el, element, 'should pass element to a condition function');
return 1 + 1 === 1;
});
t.equal(el.className, '', 'should remove class if condition is a function returning false');
var nonsense = [undefined, null, NaN, 0, ''];
for (var i = 0, l = nonsense.length; i < l; i++) {
el = getEl('ok')
toggleClass(el, nonsense[i]);
t.equal(el.className, 'ok', 'should be reasonably tolerant of nonsense');
}
el = getEl(' oh hello there ');
var clsName = el.className;
t.equal(clsName, toggleClass(el, 'hello', true).className, 'should not touch classNames if not modifying');
// toggling with conditions
el = getEl('oh');
t.equal(el, toggleClass(el, 'hi', true), 'should always return element');
el = getEl('oh');
t.equal(el, toggleClass(el, 'hi', false), 'should always return element');
// toggling without conditions
el = getEl('oh');
t.equal(el, toggleClass(el, 'oh'), 'should always return element');
el = getEl('oh');
t.equal(el, toggleClass(el, 'hi'), 'should always return element');
t.end();
});
amp-jump
If you pass an array and an item in that array, returns the item immediately following that item.
Optionally, pass a number to specify how much to jump by (defaults to 1). This number can be positive or negative.
If item is not in array, returns undefined.
If you jump out of range, returns undefined unless you pass true as the loop argument. In which case it loops around to other end.
Example
var jump = require('amp-jump');
var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
// basic case
jump(arr, 'a'); //=> 'b'
jump(arr, 'a', 3); //=> 'd'
// returns undefined if out of range
jump(arr, 'g'); //=> undefined
// negative jump values work fine
jump(arr, 'b', -1); //=> 'a'
// if you want to loop around, pass `true` as `loop` argument
jump(arr, 'a', -1, true); //=> 'g'
jump(arr, 'a', 7, true); //=> 'a'
// returns `undefined` if the item passed
// isn't found in the list at all
jump(arr, 'z'); //=> undefined
var isNumber = require('amp-is-number');
var isArray = require('amp-is-array');
var indexOf = require('amp-index-of');
module.exports = function jump(array, currentItem, jumpSize, loop) {
if (!isArray(array)) return array;
var index = indexOf(array, currentItem);
if (index === -1) {
return;
}
if (!isNumber(jumpSize)) {
jumpSize = 1;
}
var len = array.length;
var newIndex = index + jumpSize;
// we jumped too far
if (newIndex > (len - 1)) {
return loop ? array[newIndex % len] : undefined;
}
// we're negative
if (newIndex < 0) {
if (!loop) return;
newIndex = len + (newIndex % len);
if (newIndex === len) {
newIndex = 0;
}
}
// return our new item
return array[newIndex];
};
var test = require('tape');
var jump = require('./jump');
test('amp-jump', function (t) {
var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
var obj = {};
t.equal(jump(obj), obj, 'should return passed value for non arrays');
t.equal(jump(1), 1, 'should return passed value for simple values');
t.equal(jump(arr, 'x'), undefined, 'returns `undefined` if passed item not present');
t.equal(jump(arr, 'a', -1), undefined, 'returns `undefined` if out of range low end');
t.equal(jump(arr, 'a', 7), undefined, 'returns `undefined` if out of range on high end');
t.equal(jump(arr, 'a'), 'b', '`jump` defaults to `1`');
t.equal(jump(arr, 'a', 1), 'b', 'jumps work');
t.equal(jump(arr, 'a', 3), 'd', 'jumps work');
t.equal(jump(arr, 'c', -2), 'a', 'works for negative `jump`');
// with loop argument
t.equal(jump(arr, 'g', null, true), 'a', 'loops around to front if not given a jump');
t.equal(jump(arr, 'g', 1, true), 'a', 'loops around to front');
t.equal(jump(arr, 'g', 5, true), 'e', 'loops around to front with jumps');
t.equal(jump(arr, 'g', 706, true), 'f', 'loops around to front with even larger jumps');
t.equal(jump(arr, 'a', -1, true), 'g', 'loops around to back');
t.equal(jump(arr, 'a', -5, true), 'c', 'loops around to back with slightly larger jumps');
t.equal(jump(arr, 'a', -705, true), 'c', 'loops around to back with much larger jumps');
t.equal(jump(arr, 'a', 0), 'a', 'should work for zero');
t.equal(jump(arr, 'd', 0), 'd', 'should work for zero at any point');
t.equal(jump(arr, 'g', 0), 'g', 'should work for zero at end');
t.end();
});
amp-sorted-insert
Inserts an item onto an already sorted array (or array-like object) in a way that leaves the
array sorted and returns the index at which the new item was inserted.
Uses a binary search using the new item and the items in the array to
determine where to put the new item.
Sorting is done either by raw comparison (<, >, =) or via a
comparator which can be a string representing a named attribute of the
itme, or a function that will be given one item and will return the
value to be used in comparisons.
Example
var sortedInsert = require('amp-sorted-insert');
//Using native compare
var items = [1,3,5,7]; //Already sorted
sortedInsert(items, 2); //[1,2,3,5,7]
//Using string comparator
var people = [
{name: 'Robert', rank: 2},
{name: 'Sally', rank: 4}
]; //Already sorted by rank
var pat = {name: 'Pat', rank: 1};
sortedInsert(people, pat, 'rank'); //[{name: 'Pat', rank:1}, {name: 'Robert', rank: 2}, {name: 'Sally', rank: 4}]
//Using function comparator
function nameLength(person) {
return person.name.length;
}
var people = [
{name: 'Pat', rank: 1},
{name: 'Robert', rank: 2}
]; //Already sorted by name length
var sally = {name: 'Sally', rank: 4};
sortedInsert(people, sally, nameLength); //[{name: 'Pat', rank: 1}, {name: 'Sally', rank: 4}, {name: 'Robert', rank: 2}]
var sortedIndex = require('amp-sorted-index');
var splice = Array.prototype.splice;
module.exports = function sortedInsert(array, item) {
var index = sortedIndex.apply(this, arguments);
splice.call(array, index, 0, item);
return index;
};
var test = require('tape');
var sortedInsert = require('./sorted-insert');
test('amp-sorted-insert', function (t) {
var numbers = [1,3,5,7];
sortedInsert(numbers, 2);
t.equal(numbers.length, 5);
t.equal(numbers[1], 2);
var robert = {name: 'Robert', rank: 2};
var sally = {name: 'Sally', rank: 4};
var pat = {name: 'Pat', rank: 1};
var people = [robert, sally];
sortedInsert(people, pat, 'rank');
t.deepEqual(people, [pat, robert, sally]);
people = [pat, robert];
sortedInsert(people, sally, function (person) { return person.name.length; });
t.deepEqual(people, [pat, sally, robert]);
var arrayLike = {
0: 'a',
1: 'b',
length: 2
};
var index = sortedInsert(arrayLike, 'c');
t.equal(index, 2, 'should be inserted at 2');
t.deepEqual(arrayLike, {
0: 'a',
1: 'b',
2: 'c',
length: 3
}, 'should modify array-like object');
t.end();
});
amp-array-to-readable
Takes an array of values and returns a readable string separated with comma and conjunction if length warrants it.
Example
var arrayToReadable = require('amp-array-to-readable');
arrayToReadable([1, 2, 3]); //=> '1, 2, and 3'
arrayToReadable([1, 2]); //=> '1 and 2'
arrayToReadable([1]); //=> '1'
var words = ['one', 'two', 'three'];
arrayToReadable(words); //=> 'one, two, and three'
arrayToReadable(words, {conjunction: 'or'}); //=> 'one, two, or three'
arrayToReadable(words, {oxford: false}); //=> 'one, two and three'
module.exports = function arrayToReadable(arr, opts) {
if (!opts) {
opts = {};
}
var length = arr.length;
var conj = opts.conjunction || 'and';
var oxford = opts.oxford !== false;
switch(length) {
case 1:
return arr[0].toString();
case 2:
return arr.join(' ' + conj + ' ');
default:
return arr.slice(0, -1).join(', ') + (oxford ? ',' : '') + ' ' + conj + ' ' + arr.slice(-1);
}
};
var test = require('tape');
var arrayToReadable = require('./array-to-readable');
test('amp-array-to-readable', function (t) {
t.equal(arrayToReadable([1, 2, 3]), '1, 2, and 3');
t.equal(arrayToReadable([1, 2]), '1 and 2');
t.equal(arrayToReadable([1]), '1');
t.equal(arrayToReadable(['one', 'two', 'three', 'four']), 'one, two, three, and four');
t.equal(arrayToReadable(['one', 'two', 'three', 'four'], {conjunction: 'or'}), 'one, two, three, or four');
t.equal(arrayToReadable(['one', 'two', 'three', 'four'], {oxford: false}), 'one, two, three and four');
t.end();
});
amp-create-callback
Internal amp utility for creating callbacks used in looping functions such as map and filter (mainly used internally). Having a known argument count makes it more optimizable by browsers.
Example
var createCallback = require('amp-create-callback');
var callback = createCallback(function () {
// something
}, context, 3);
[1, 2, 3].map(callback);
var test = require('tape');
var createCallback = require('./create-callback');
test('amp-create-callback', function (t) {
var argLength = 1;
var context = {hi: 'there'};
var callback = createCallback(function () {
t.equal(this, context, 'should maintain context');
t.equal(arguments.length, 1, 'should never have more than arg length passed');
}, context, 1);
callback(1, 2, 3, 4, 5);
callback = createCallback(function () {
t.equal(this, context, 'should maintain context');
t.equal(arguments.length, 2, 'should never have more than arg length passed');
}, context, 2);
callback(1, 2, 3, 4, 5);
callback = createCallback(function () {
t.equal(this, context, 'should maintain context');
t.equal(arguments.length, 3, 'should never have more than arg length passed');
}, context, 3);
callback(1, 2, 3, 4, 5);
callback = createCallback(function () {
t.equal(this, context, 'should maintain context');
t.equal(arguments.length, 4, 'should never have more than arg length passed');
}, context, 4);
callback(1, 2, 3, 4, 5);
callback = createCallback(function () {
t.equal(arguments.length, 5, 'should have as many as passed');
});
callback(1, 2, 3, 4, 5);
t.end();
});
amp-internal-flatten
An internal flatten implementation for re-use.
Takes following args:
value: initial array
shallow: whether to flatten recursively or just one level
strict: if strict is true it won't keep any values other than arrays an arguments
output: array or array-like object we're pushing to and will return
Example
var internalFlatten = require('amp-internal-flatten');
var arr = [1, [1], [[1]], 'something'];
// shallow
internalFlatten(arr, true, false, []);
//=> [1, 1, [1], 'something'];
// recursive
internalFlatten(arr, false, false, []);
//=> [1, 1, 1, 'something'];
// recursive + strict
// these options *always* will
// result an empty array as output
internalFlatten(arr, false, true, []);
//=> [];
// shallow + strict
// any non-arrays are removed
// but only run through once
// so only 2ns and 3rd args make it
// but are flattened one level
internalFlatten(arr, true, true, []);
//=> [1, [1]];
var isArray = require('amp-is-array');
var isArguments = require('amp-is-arguments');
var flatten = function flatten(input, shallow, strict, startIndex) {
var output = [], idx = 0, value;
for (var i = startIndex || 0, length = input && input.length; i < length; i++) {
value = input[i];
if (value && value.length >= 0 && (isArray(value) || isArguments(value))) {
//flatten current level of array or arguments object
if (!shallow) value = flatten(value, shallow, strict);
var j = 0, len = value.length;
output.length += len;
while (j < len) {
output[idx++] = value[j++];
}
} else if (!strict) {
output[idx++] = value;
}
}
return output;
};
module.exports = flatten;