-
-
Save PAEz/f446623dba9c494f10580aef9abdc17d to your computer and use it in GitHub Desktop.
Mini jQuery, sort of.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A collection of helper prototype for everyday DOM traversal, manipulation, | |
* and event binding. Sort of a minimalist jQuery, mainly for demonstration | |
* purposes. MIT @ m3g4p0p | |
*/ | |
window.$ = (function (undefined) { | |
/** | |
* Duration constants | |
* @type {Object} | |
*/ | |
const DURATION = { | |
DEFAULT: 500 | |
} | |
/** | |
* Style constants | |
* @type {Object} | |
*/ | |
const STYLE = { | |
SHOW: 'block', | |
HIDE: 'none' | |
} | |
/** | |
* Style property constants | |
* @type {Object} | |
*/ | |
const PROPERTY = { | |
DISPLAY: 'display' | |
} | |
/** | |
* Type constants | |
* @type {Object} | |
*/ | |
const TYPE = { | |
FUNCTION: 'function', | |
STRING: 'string' | |
} | |
/** | |
* Map elements to events | |
* @type {WeakMap} | |
*/ | |
const eventMap = new WeakMap() | |
/** | |
* Function to fade an element | |
* @param {HTMLElement} element | |
* @param {Number} from | |
* @param {Number} to | |
* @param {Number} duration | |
* @param {Function} callback | |
* @return {void} | |
*/ | |
const fade = function fade (element, from, to, duration, callback) { | |
const start = window.performance.now() | |
element.style.display = STYLE.SHOW | |
window.requestAnimationFrame(function step (timestamp) { | |
const progress = timestamp - start | |
element.style.opacity = from + (progress / duration) * (to - from) | |
if (progress < duration) { | |
window.requestAnimationFrame(step) | |
} else { | |
if (element.style.opacity <= 0) { | |
element.style.display = STYLE.HIDE | |
} | |
if (callback) { | |
callback.call(element) | |
} | |
} | |
}) | |
} | |
/** | |
* The methods in the collection'S prototype | |
* @type {Object} | |
*/ | |
const prototype = { | |
/////////////// | |
// Traversal // | |
/////////////// | |
/** | |
* Check if a node is already contained in the collection | |
* @param {HTMLElement} element | |
* @return {Boolean} | |
*/ | |
has (element) { | |
return Array.from(this).includes(element) | |
}, | |
/** | |
* Add an element or a list of elements to the collection | |
* @param {mixed} element | |
* @returns {this} | |
*/ | |
add (element) { | |
const elements = element.length !== undefined ? element : [element] | |
Array.from(elements).forEach(element => { | |
if (element && !this.has(element)) { | |
Array.prototype.push.call(this, element) | |
} | |
}) | |
return this | |
}, | |
/** | |
* Find descendants of the current collection matching a selector | |
* @param {String} selector | |
* @return {this} | |
*/ | |
find (selector) { | |
return Array.from(this).reduce( | |
(carry, element) => carry.add(element.querySelectorAll(selector)), | |
Object.create(prototype) | |
) | |
}, | |
/** | |
* Filter the current collection by a selector or filter function | |
* @param {String|Function} selector | |
* @return {this} | |
*/ | |
filter (selector) { | |
return Object.create(prototype).add( | |
Array.from(this).filter( | |
typeof selector === TYPE.FUNCTION | |
? selector | |
: element => element.matches(selector) | |
) | |
) | |
}, | |
/** | |
* Get a collection containing the adjecent next siblings | |
* of the current collection, optionally filtered by a selector | |
* @param {String|undefined} selector | |
* @return {this} | |
*/ | |
next (selector) { | |
return Object.create(prototype).add( | |
Array.from(this) | |
.map(element => element.nextElementSibling) | |
.filter(element => element && (!selector || element.matches(selector))) | |
) | |
}, | |
/** | |
* Get a collection containing the adjecent previous siblings | |
* of the current collection, optionally filtered by a selector | |
* @param {String|undefined} selector | |
* @return {this} | |
*/ | |
prev (selector) { | |
return Object.create(prototype).add( | |
Array.from(this) | |
.map(element => element.previousElementSibling) | |
.filter(element => element && (!selector || element.matches(selector))) | |
) | |
}, | |
/** | |
* Get a collection containing the immediate parents of | |
* the current collection, optionally filtered by a selector | |
* @param {String|undefined} selector | |
* @return {this} | |
*/ | |
parent (selector) { | |
return Object.create(prototype).add( | |
Array | |
.from(this) | |
.map(element => element.parentNode) | |
.filter(element => !selector || element.matches(selector)) | |
) | |
}, | |
/** | |
* Get a collection containing the immediate parents of the | |
* current collection, or, if a selector is specified, the next | |
* ancestor that matches that selector | |
* @param {String|undefined} selector | |
* @return {this} | |
*/ | |
parents (selector) { | |
return Object.create(prototype).add( | |
Array.from(this).map(function walk (element) { | |
const parent = element.parentNode | |
return parent && (!selector || parent.matches(selector)) | |
? parent | |
: walk(parent) | |
}) | |
) | |
}, | |
/** | |
* Get a collection containing the immediate children of the | |
* current collection, optionally filtered by a selector | |
* @param {String|undefined} selector | |
* @return {this} | |
*/ | |
children (selector) { | |
return Object.create(prototype).add( | |
Array | |
.from(this) | |
.reduce((carry, element) => carry.concat(...element.children), []) | |
.filter(element => !selector || element.matches(selector)) | |
) | |
}, | |
////////////////// | |
// Manipulation // | |
////////////////// | |
/** | |
* Add a class to all elements in the current collection | |
* @param {String} className | |
* @returns {this} | |
*/ | |
addClass (className) { | |
Array.from(this).forEach(el => { | |
el.classList.add(className) | |
}) | |
return this | |
}, | |
/** | |
* Remove a class from all elements in the current collection | |
* @param {String} className | |
* @return {this} | |
*/ | |
removeClass (className) { | |
Array.from(this).forEach(el => { | |
el.classList.remove(className) | |
}) | |
return this | |
}, | |
/** | |
* Set the value property of all elements in the current | |
* collection, or, if no value is specified, get the value | |
* of the first element in the collection | |
* @param {mixed} newVal | |
* @return {this} | |
*/ | |
val (newVal) { | |
if (!newVal) { | |
return this[0].value | |
} | |
Array.from(this).forEach(el => { | |
el.value = newVal | |
}) | |
return this | |
}, | |
/** | |
* Set the HTML of all elements in the current collection, | |
* or, if no markup is specified, get the HTML of the first | |
* element in the collection | |
* @param {String|undefined} newHtml | |
* @return {this} | |
*/ | |
html (newHtml) { | |
if (!newHtml) { | |
return this[0].innerHtml | |
} | |
Array.from(this).forEach(el => { | |
el.innerHtml = newVal | |
}) | |
return this | |
}, | |
/** | |
* Set the text of all elements in the current collection, | |
* or, if no markup is specified, get the HTML of the first | |
* element in the collection | |
* @param {String|undefined} newText | |
* @return {this} | |
*/ | |
text (newText) { | |
if (!newText) { | |
return this[0].textContent | |
} | |
Array.from(this).forEach(el => { | |
el.textContent = newText | |
}) | |
return this | |
}, | |
/////////////////////// | |
// CSS and animation // | |
/////////////////////// | |
/** | |
* Hide all elements in the current collection | |
* @return {this} | |
*/ | |
hide () { | |
Array.from(this).forEach(element => { | |
element.style.display = null | |
if (window.getComputedStyle(element).getPropertyValue(PROPERTY.DISPLAY) !== STYLE.HIDE) { | |
element.style.display = STYLE.HIDE | |
} | |
}) | |
return this | |
}, | |
/** | |
* Show all elements in the current collection | |
* @return {this} | |
*/ | |
show () { | |
Array.from(this).forEach(element => { | |
element.style.display = null | |
if (window.getComputedStyle(element).getPropertyValue(PROPERTY.DISPLAY) === STYLE.HIDE) { | |
element.style.display = STYLE.SHOW | |
} | |
}) | |
return this | |
}, | |
/** | |
* Set the CSS of the elements in the current collection | |
* by either specifying the CSS property and value, or | |
* an object containing the style declarations | |
* @param {String|object} style | |
* @param {mixed} value | |
* @return {this} | |
*/ | |
css (style, value) { | |
const currentStyle = {} | |
if (typeof style === TYPE.STRING) { | |
if (!value) { | |
return this[0] && window | |
.getComputedStyle(this[0]) | |
.getPropertyValue(style) | |
} | |
currentStyle[style] = value | |
} else { | |
Object.assign(currentStyle, style) | |
} | |
Array.from(this).forEach(element => { | |
Object.assign(element.style, currentStyle) | |
}) | |
return this | |
}, | |
/** | |
* Fade the elements in the current collection in; optionally | |
* takes the fade duration and a callback that gets executed | |
* on each element after the animation finished | |
* @param {Number|undefined} duration | |
* @param {Function|undefined} callback | |
* @return {this} | |
*/ | |
fadeIn (duration, callback) { | |
Array.from(this).forEach(element => { | |
fade(element, 0, 1, duration || DURATION.DEFAULT, callback) | |
}) | |
return this | |
}, | |
/** | |
* Fade the elements in the current collection out; optionally | |
* takes the fade duration and a callback that gets executed | |
* on each element after the animation finished | |
* @param {Number|undefined} duration | |
* @param {Function|undefined} callback | |
* @return {this} | |
*/ | |
fadeOut (duration, callback) { | |
Array.from(this).forEach(element => { | |
fade(element, 1, 0, duration || DURATION.DEFAULT, callback) | |
}) | |
return this | |
}, | |
//////////// | |
// Events // | |
//////////// | |
/** | |
* Bind event listeners to all elements in the current collection, | |
* optionally delegated to a target element specified as 2nd argument | |
* @param {String} type | |
* @param {Function|String} target | |
* @param {Function|undefined} callback | |
* @return {this} | |
*/ | |
on (type, target, callback) { | |
const handler = callback | |
? function (event) { | |
if (event.target.matches(target)) { | |
callback.call(this, event) | |
} | |
} | |
: target | |
Array.from(this).forEach(element => { | |
const events = eventMap.get(element) || eventMap.set(element, {}).get(element) | |
events[type] = events[type] || [] | |
events[type].push(handler) | |
element.addEventListener(type, handler) | |
}) | |
return this | |
}, | |
/** | |
* Remove event listeners from the elements in the current | |
* collection; if no handler is specified, all listeners of | |
* the given type will be removed | |
* @param {String} type | |
* @param {Function|undefined} callback | |
* @return {this} | |
*/ | |
off (type, callback) { | |
Array.from(this).forEach(element => { | |
const events = eventMap.get(element) | |
const callbacks = events && events[type] | |
if (callback) { | |
element.removeEventListener(type, callback) | |
if (callbacks) { | |
events[type] = callbacks.filter(current => current !== callback) | |
} | |
} else if (callbacks) { | |
delete events[type] | |
callbacks.forEach(callback => { | |
element.removeEventListener(type, callback) | |
}) | |
} | |
}) | |
return this | |
}, | |
/////////////////// | |
// Miscellaneous // | |
/////////////////// | |
/** | |
* Execute a funtion on each element in the current collection | |
* @param {Function} fn | |
* @return {this} | |
*/ | |
each (fn) { | |
Array.from(this).forEach(element => { | |
fn.call(element) | |
}) | |
return this | |
} | |
} | |
/** | |
* Create a new collection | |
* @param {String} selector | |
* @param {HTMLElement|undefined} context | |
* @return {Object} | |
*/ | |
return function createCollection (selector, context) { | |
const initial = typeof selector === TYPE.STRING | |
? (context || document).querySelectorAll(selector) | |
: selector | |
const instance = Object.create(prototype) | |
return initial | |
? instance.add(initial) | |
: instance | |
} | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment