(function() {
'use strict';
// we'll use this to keep track of registered event listeners
// eslint-disable-next-line no-unused-vars
const event_listeners = new Map();
// we'll use this when generating random IDs
// eslint-disable-next-line no-unused-vars
let internal_counter = 0;
// this is the function used internally to create ZebraJS objects using the given arguments
// at the end of this file we give it a simpler name, like "$", but internally we'll use it like it is
/**
* Creates a ZebraJS object which provides methods meant for simplifying the interaction with the set of
* elements matched by the `selector` argument. This is referred to as `wrapping` those elements.
*
* @example
*
* // select an element by ID
* const element = $('#foo');
*
* // select element by class name
* const elements = $('.foo');
*
* // select elements by using JavaScript
* const elements = $(document.querySelectorAll('.foo'));
*
* // use CSS selectors
* const elements = $('input[type=text]');
*
* // create elements
* const element = $('<div>').addClass('foo').appendTo($('body'));
*
* // SECURITY WARNING: When creating HTML elements from strings, NEVER use untrusted user input directly.
* // This could lead to Cross-Site Scripting (XSS) attacks.
*
* // UNSAFE - user input could contain malicious code:
* // const userInput = '<img src=x onerror=alert(1)>';
* // $(userInput); // XSS vulnerability!
*
* // SAFE - create elements programmatically and set user data via methods:
* // $('<div>').text(userInput); // User input is safely escaped
* // $('<img>').attr('src', userInput); // Safely set attributes
*
* // SAFE - sanitize user input with a library like DOMPurify before creating HTML:
* // const sanitized = DOMPurify.sanitize(userInput);
* // $(sanitized);
*
* @param {mixed} selector A selector to filter DOM elements from the current document. It can be a
* query selector, a {@link ZebraJS} object, a DOM element, a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/NodeList NodeList},
* and array of DOM elements<br><br>Alternatively, it can be a HTML tag
* to create.<br>
* <blockquote>**Never pass unsanitized user input when creating HTML elements,
* as this can lead to XSS vulnerabilities!**</blockquote>
*
* @param {mixed} [parent] A selector to filter DOM elements from the current document, but only
* those which have as parent the element(s) indicated by this argument. It
* can be a query selector, a {@link ZebraJS} object, a DOM element, a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/NodeList NodeList},
* and array of DOM elements
*
* @param {boolean} [first_only] Setting this argument will instruct the method to return only the first
* element from the set of matched elements.
*
* @return {array} Returns a special array holding the matching elements and having all the methods to help
* you work with those elements.
*
* @author Stefan Gabos <contact@stefangabos.ro>
* @version 2.0.0 (last revision December 23, 2025)
* @copyright (c) 2016-2025 Stefan Gabos
* @license LGPL-3.0
* @alias ZebraJS
* @class
*/
$ = function(selector, parent, first_only) {
let elements = [], result;
// refer to document.body node if it is the case
if (typeof selector === 'string' && selector.toLowerCase() === 'body') selector = document.body;
// if selector is given as a string
if (typeof selector === 'string')
// if it seems that we want to *create* an HTML node
if (selector.startsWith('<') && selector.indexOf('>') > 1 && selector.length > 2) {
// create a dummy container
parent = document.createElement('div');
// set its body to the selector string
parent.innerHTML = selector;
// add created node to the elements array
elements.push(parent.firstChild);
// if we want to select elements
} else {
// if parent is not given, consider "document" to be the parent
if (!parent) parent = document;
// if parent is set and is a ZebraJS object, refer to the first DOM element from the set instead
else if (typeof parent === 'object' && parent.version) parent = parent[0];
// if parent is set and is a string, refer to the matching DOM element
else if (typeof parent === 'string') parent = document.querySelector(parent);
// if the selector is an ID
// select the matching element and add it to the elements array
if (selector.match(/^\#[^\s]+$/)) elements.push(parent.querySelector(selector));
// if the "first_only" argument is set
else if (first_only) {
result = _query(selector, parent, 'first');
if (result) elements.push(result);
// if we need all elements
} else elements = _query(selector, parent);
}
// if selector is the Document object, the Window object, a DOM node or a text node
else if (typeof selector === 'object' && (selector instanceof Document || selector instanceof Window || selector instanceof Element || selector instanceof Text))
// add it to the elements array
elements.push(selector);
// if selector is a NodeList (returned by document.querySelectorAll), add items to the elements array
else if (selector instanceof NodeList) elements = Array.from(selector);
// if selector is an array of DOM elements, add them to the elements array
else if (Array.isArray(selector)) elements = [...selector];
// if the selector is a ZebraJS object, simply return it
else if (typeof selector === 'object' && selector.version) return selector;
// remove undefined values
elements = elements.filter(value => value !== undefined && value !== null);
// attach all the ZebraJS methods to the elements array (including plugins, if any)
Object.assign(elements, $.fn);
// return the elements "array-on-steroids"
return elements;
}
$.fn = {
// zebrajs version
version: '2.0.0'
};
/**
* Selector engine that handles both standard CSS selectors and jQuery pseudo-selectors.
*
* Supported pseudo-selectors:
*
* - :first - First element in the set
* - :last - Last element in the set
* - :even - Even-indexed elements (0, 2, 4...)
* - :odd - Odd-indexed elements (1, 3, 5...)
* - :eq(n) - Element at index n (supports negative indices)
* - :gt(n) - Elements after index n
* - :lt(n) - Elements before index n
* - :has(selector) - Elements containing a descendant matching selector
* - :contains(text) - Elements containing the specified text
* - :visible - Visible elements
* - :hidden - Hidden elements
* - :parent - Elements that have children
* - :header - All header elements (h1-h6)
* - :input - All form input elements
* - :text, :checkbox, :radio, :password, :submit, :reset, :button, :file, :image - Input types
*
* @param {string} selector The selector string (CSS or jQuery pseudo-selector)
*
* @param {Element} [context] The element to search within
* <br><br>
* Default is `document`
*
* @param {string} [mode] The mode in which to use the function.
* <br><br>
* Possible values are:
* <br><br>
* - `all` (default) - the function returns `all` matched elements<br>
* - `first` - the function returns the first element from the set<br>
* - `matches` - the function works like JavaScript's native .matches() method
*
* @return {array|Element|null|boolean} Based on mode:
* <br><br>
* - `all` - returns an array of elements (empty array if none found)<br>
* - `first` - returns first element or null if none found<br>
* - `matches` - returns `TRUE` or `FALSE` based on whether the element matches or not the selector
*
* @private
*/
const _query = (selector, context = document, mode = 'all') => {
// helper to warn about invalid selectors
const err = (sel, msg) => {
if (typeof console !== 'undefined' && console.warn)
console.warn(`ZebraJS: Invalid selector "${sel}"${msg ? ', ' + msg : ''}`);
};
// default to "all" mode
if (!mode || (mode !== 'first' && mode !== 'matches')) mode = 'all';
// pattern to match jQuery pseudo-selectors at the end of the selector
// this handles the most common case: "div.foo:first", "ul > li:even", etc.
const pseudo_pattern = /:(first|last|even|odd|eq|gt|lt|has|contains|visible|hidden|parent|header|input|text|checkbox|radio|password|submit|reset|button|file|image)\s*(\([^)]*\))?$/;
let elements = [], filtered = [];
try {
// if non-string or empty selector stop early
if (typeof selector !== 'string' || selector.trim() === '') return mode === 'first' ? null : [];
selector = selector.trim();
let match;
// if the selector contains pseudo-selectors
if ((match = selector.match(pseudo_pattern)) !== null) {
// the pseudo_name is the pseudo-selector (i.e. "first")
// pseudo_arg will be the argument of the pseudo selector (if any) (
// i.e the "text" in ":contains(text)"
// base_selector will be the base CSS selector (if any) that precedes the pseudo-selector
// i.e. the "div > li" in "div > li:first"
const [, pseudo_name, pseudo_arg_raw] = match;
const pseudo_arg = pseudo_arg_raw ? pseudo_arg_raw.slice(1, -1) : null;
let base_selector = selector.replace(pseudo_pattern, '').trim();
// if base selector is empty after removing pseudo, use universal selector
if (!base_selector) base_selector = '*';
// if we are in "matches" mode
if (mode === 'matches') {
// if we have a positional pseudo-selector
if (['first', 'last', 'even', 'odd', 'eq', 'gt', 'lt'].includes(pseudo_name)) {
err(`:${pseudo_name}`, 'positional pseudo-selectors not supported in .matches() context');
return false;
}
// test if the context element matches the base selector
try {
// if
if (
// base selector is just '*'
base_selector === '*' ||
// or context matches the base selector
context.matches(base_selector)
// it matches everything
) elements = [context];
// doesn't match, empty array
else elements = [];
} catch (e) {
err(base_selector, e.message);
return false;
}
// for the other modes, search for elements in context
} else
try {
// get all elements that match the base CSS selector
elements = Array.from(context.querySelectorAll(base_selector));
// if base selector is invalid, return null to fall back to normal error handling
} catch (e) {
err(base_selector, e.message);
return mode === 'first' ? null : [];
}
// apply the jQuery pseudo-selector filter
switch (pseudo_name) {
case 'first':
if (elements.length > 0) filtered = [elements[0]];
break;
case 'last':
if (elements.length > 0) filtered = [elements[elements.length - 1]];
break;
case 'even':
filtered = elements.filter((_el, index) => index % 2 === 0);
break;
case 'odd':
filtered = elements.filter((_el, index) => index % 2 === 1);
break;
case 'eq':
case 'gt':
case 'lt':
// we treat the argument as a numeric value
let index = parseInt(pseudo_arg, 10);
// ...but if converting using parseInt yields "NaN" stop now
if (isNaN(index)) break;
if (pseudo_name === 'eq') {
// we're supporting negative indices (count from end)
if (index < 0) index = Math.max(0, elements.length + index);
// if value is in range, extract the element at position, or empty array otherwise
if (index >= 0 && index < elements.length) {
filtered = [elements[index]];
break;
}
}
// extract the matching elements otherwise, or whatever is already in elements if nothing matches
filtered = elements.filter((_el, idx) => (pseudo_name === 'gt' && idx > index) || (pseudo_name === 'lt' && idx < index));
break;
// elements that contain a descendant matching the selector
case 'has':
// only if the argument is available
if (pseudo_arg !== null && pseudo_arg !== undefined)
// filter for a match
filtered = elements.filter(el => {
try {
return el.querySelectorAll(pseudo_arg).length > 0;
} catch (e) {
return false;
}
});
break;
// elements containing the specified text (case-sensitive)
case 'contains':
// only if the argument is available
if (pseudo_arg !== null && pseudo_arg !== undefined)
// filter for a match
filtered = elements.filter(el => el.textContent.includes(pseudo_arg));
break;
// elements that are visible (not display:none, visibility:hidden, or opacity:0)
case 'visible':
// filter elements
filtered = elements.filter(el => {
// if element doesn't take up space
if (el.offsetWidth === 0 && el.offsetHeight === 0) return false;
// check visibility
const style = window.getComputedStyle(el);
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0';
});
break;
// elements that are hidden (opposite of :visible)
case 'hidden':
// filter elements
filtered = elements.filter(el => {
// if element takes up space
if (el.offsetWidth === 0 && el.offsetHeight === 0) return true;
// check visibility
const style = window.getComputedStyle(el);
return style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0';
});
break;
// elements that have children (element or text nodes)
case 'parent':
filtered = elements.filter(el => el.childNodes.length > 0);
break;
// all header elements (h1-h6)
case 'header':
filtered = elements.filter(el => /^H[1-6]$/.test(el.tagName));
break;
// all form input elements
case 'input':
filtered = elements.filter(el => /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(el.tagName));
break;
// input type pseudo-selectors
case 'text':
case 'checkbox':
case 'radio':
case 'password':
case 'submit':
case 'reset':
case 'button':
case 'file':
case 'image':
filtered = elements.filter(el => el.tagName === 'INPUT' && el.type === pseudo_name);
break;
default:
filtered = elements;
}
// when in "matches" mode, return now
if (mode === 'matches') return filtered.length > 0;
// return based on whether the "mode" argument is set to "first" or "all"
return mode === 'first' ? (filtered.length > 0 ? filtered[0] : null) : filtered;
}
// no pseudo-selector found, use native CSS selectors
// if in "matches" mode
if (mode === 'matches')
// test if context element matches the selector
try {
return context.matches(selector);
} catch (e) {
err(selector, e.message);
return false;
}
// for "all" and "first" modes, search for elements
return mode === 'first' ? context.querySelector(selector) : Array.from(context.querySelectorAll(selector));
} catch (e) {
err(selector, e.message);
return mode === 'matches' ? false : (mode === 'first' ? null : []);
}
};
/**
* Private helper method used by {@link ZebraJS#addClass .addClass()}, {@link ZebraJS#removeClass .removeClass()} and
* {@link ZebraJS#toggleClass .toggleClass()} methods.
*
* @param {string} action What to do with the class(es)
* <br><br>
* Possible values are `add`, `remove` and `toggle`.
*
* @param {string} class_names One or more space-separated class names to be added/removed/toggled for each element
* in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements (the parents, not the appended elements), for chaining.
*
* @access private
*/
$.fn._class = function(action, class_names) {
// split by space and create an array
class_names = class_names.split(' ');
// iterate through the set of matched elements
this.forEach(element => {
// iterate through the class names to add
class_names.forEach(class_name => {
// add or remove class(es)
element.classList[action === 'add' || (action === 'toggle' && !element.classList.contains(class_name)) ? 'add' : 'remove'](class_name);
});
});
// return the set of matched elements, for chaining
return this;
}
/**
* Private helper method used by {@link ZebraJS#clone .clone()} method when called with the `deep_with_data_and_events`
* argument set to TRUE. It recursively attaches events and data from an original element's children to its clone
* children.
*
* @param {DOM_element} element Element that was cloned
*
* @param {DOM_element} clone Clone of the element
*
* @return {void}
*
* @access private
*/
$.fn._clone_data_and_events = function(element, clone) {
// get the original element's and the clone's children
const elements = Array.from(element.children),
clones = Array.from(clone.children),
$this = this;
// if the original element's has any children
if (elements && elements.length)
// iterate over the original element's children
elements.forEach((element, index) => {
// iterate over all the existing event listeners
event_listeners.forEach((listeners, event_type) => {
// iterate over the events of current type
listeners.forEach(properties => {
// if this is an event attached to element we've just cloned
if (properties[0] === element) {
// also add the event to the clone element
$(clones[index]).on(event_type + (properties[2] ? '.' + properties[2] : ''), properties[1]);
// if original element has some data attached to it
if (element.zjs && element.zjs.data) {
// clone it
clones[index].zjs = {};
clones[index].zjs.data = element.zjs.data;
}
}
});
});
// recursively attach events to children's children
$this._clone_data_and_events(element, clones[index]);
});
}
/**
* Private helper method used by {@link ZebraJS#append .append()}, {@link ZebraJS#appendTo .appendTo()},
* {@link ZebraJS#after .after()}, {@link ZebraJS#insertAfter .insertAfter()}, {@link ZebraJS#before .before()},
* {@link ZebraJS#insertBefore .insertBefore()}, {@link ZebraJS#prepend .prepend()}, {@link ZebraJS#prependTo .prependTo()}
* and {@link ZebraJS#wrap .wrap()} methods.
*
* @param {mixed} content Depending on the caller method this is the DOM element, text node, HTML string, or
* {@link ZebraJS} object to insert in the DOM.
*
* @param {string} where Indicated where the content should be inserted, relative to the set of matched elements.
* <br><br>
* Possible values are `after`, `append`, `before`, `prepend` and `wrap`.
*
* @return {ZebraJS} Returns the set of matched elements (the parents, not the appended elements), for chaining.
*
* @access private
*/
$.fn._dom_insert = function(content, where) {
const $this = this;
// make a ZebraJS object out of whatever given as content
content = $(content);
// iterate through the set of matched elements
this.forEach((element, element_index) => {
// since content is an array of DOM elements or text nodes
// iterate over the array
content.forEach(item => {
// where the content needs to be moved in the DOM
switch (where) {
// insert a clone after each target except for the last one after which we insert the original content
case 'after':
case 'replace':
case 'wrap': element.parentNode.insertBefore(element_index < $this.length - 1 ? item.cloneNode(true) : item, element.nextSibling); break;
// add a clone to each parent except for the last one where we add the original content
case 'append': element.appendChild(element_index < $this.length - 1 ? item.cloneNode(true) : item); break;
// insert a clone before each target except for the last one before which we insert the original content
case 'before': element.parentNode.insertBefore(element_index < $this.length - 1 ? item.cloneNode(true) : item, element); break;
// prepend a clone to each parent except for the last one where we add the original content
case 'prepend': element.insertBefore(element_index < $this.length - 1 ? item.cloneNode(true) : item, element.firstChild); break;
}
// if we're wrapping the element
if (where === 'wrap' || where === 'replace') {
// remove the original element
element.parentNode.removeChild(element);
// for the "wrap" method, insert the removed element back into the container
if (where === 'wrap') item.appendChild(element);
}
});
});
// return the newly inserted element(s), for chaining
return content;
}
/**
* Private helper method used by {@link ZebraJS#children .children()}, {@link ZebraJS#siblings .siblings()},
* {@link ZebraJS#next .next()} and {@link ZebraJS#prev .prev()} methods.
*
* @param {string} action Specified what type of elements to look for
* <br><br>
* Possible values are `children` and `siblings`.
*
* @param {string} selector If the selector is supplied, the elements will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns the found elements, as a ZebraJS object
*
* @access private
*/
$.fn._dom_search = function(action, selector) {
let remove_id, root, tmp;
const result = [];
const $this = this;
// iterate through the set of matched elements
this.forEach(element => {
remove_id = false;
// if selector is specified
if (selector) {
// if we're looking for children nodes, the root element is the element itself
if (action === 'children') root = element;
// otherwise, the root element is the element's parent node
else root = element.parentNode;
// if the root element doesn't have an ID,
if (null === root.getAttribute('id')) {
// generate and set a random ID for the element's parent node
root.setAttribute('id', $this._random('id'));
// set this flag so that we know to remove the randomly generated ID when we're done
remove_id = true;
}
}
// if we're looking for siblings
if (action === 'siblings') {
// cache parent node to avoid multiple property accesses
const parent = element.parentNode;
// get the element's parent's children nodes which, optionally, match a given selector
// and add them to the results array, skipping the current element
result.push(...Array.from(selector ? parent.querySelectorAll(`#${parent.id}>${selector}`) : parent.children).filter(child => child !== element));
// if we're looking for children
} else if (action === 'children')
// get the element's children nodes which, optionally, match a given selector
// and add them to the results array
result.push(...Array.from(selector ? root.querySelectorAll(`#${root.id}>${selector}`) : element.children));
// if we're looking next/previous sibling
else if (action === 'previous' || action === 'next') {
// get the next/previous sibling
tmp = element[(action === 'next' ? 'next' : 'previous') + 'ElementSibling'];
// if there's no selector specified or there is and it matches
if (!selector || $(tmp).is(selector))
// add it to the results array
result.push(tmp);
}
// if present, remove the randomly generated ID
// we remove the randomly generated ID from the element
if (remove_id) root.removeAttribute('id');
});
// return the result, as a ZebraJS object
return $(result);
}
/**
* Private helper method
*
* @access private
*/
$.fn._random = function(prefix) {
// if the internal counter is too large, reset it
if (internal_counter > Number.MAX_VALUE) internal_counter = 0;
// return a pseudo-random string by incrementing the internal counter
return `${prefix}_${internal_counter++}`;
}
/**
* Private helper method used by traversal/filtering methods to set prevObject for .end() support.
*
* @param {ZebraJS} result The ZebraJS object to set prevObject on
*
* @return {ZebraJS} Returns the result with prevObject set to this
*
* @access private
*/
$.fn._add_prev_object = function(result) {
// store reference to the previous object so .end() can restore it
result.prevObject = this;
// return the result
return result;
}
/**
* CSS properties that accept unitless numeric values.
*
* These properties don't need 'px' or other units appended when setting numeric values.
* Includes standard CSS properties, flexbox, grid, and SVG properties.
*
* @private
* @constant {Array<string>}
*/
// eslint-disable-next-line no-unused-vars
const unitless_properties = [
// Animation
'animationIterationCount',
// Border & Image
'borderImageOutset',
'borderImageSlice',
'borderImageWidth',
// Flexbox (legacy)
'boxFlex',
'boxFlexGroup',
'boxOrdinalGroup',
// Columns
'columnCount',
'columns',
// Flexbox
'flex',
'flexGrow',
'flexNegative',
'flexOrder',
'flexPositive',
'flexShrink',
// Font
'fontWeight',
'lineClamp',
'lineHeight',
// Grid
'gridArea',
'gridColumn',
'gridColumnEnd',
'gridColumnSpan',
'gridColumnStart',
'gridRow',
'gridRowEnd',
'gridRowSpan',
'gridRowStart',
// Display & Opacity
'opacity',
'order',
'orphans',
// Miscellaneous
'tabSize',
'widows',
'zIndex',
'zoom',
// SVG properties
'fillOpacity',
'floodOpacity',
'stopOpacity',
'strokeDasharray',
'strokeDashoffset',
'strokeMiterlimit',
'strokeOpacity',
'strokeWidth'
];
/**
* Performs an asynchronous HTTP (Ajax) request.
*
* @example
*
* $.ajax({
* url: 'http://mydomain.com/index.html',
* method: 'GET',
* data: {
* foo: 'baz',
* bar: 'bax'
* },
* error: () => {
* alert('error!');
* },
* success: () => {
* alert('success!');
* }
* });
*
* @param {string} [url] The URL to which the request is to be sent.<br>
* You may skip it and set it in the *options* object
*
* @param {object} options A set of key/value pairs that configure the Ajax request.
*
* | Property | Type | Description
* |-------------------|-----------------------|----------------------------------------------
* | **url** | *string* | The URL to which the request is to be sent.
* | **async** | *boolean* | By default, all requests are sent *asynchronously*. If you need synchronous requests, set this option to `false`. Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active.<br>Default is `true`
* | **beforeSend** | *function* | A pre-request callback function that can be used to modify the XMLHTTPRequest object before it is sent. Use this to set custom headers, etc. The XMLHTTPRequest object and settings objects are passed as arguments. Returning false from this function will cancel the request.
* | **cache** | *boolean* | If set to `false`, will force requested pages not to be cached by the browser. Note: Setting cache to `false` will only work correctly with `HEAD` and `GET` requests. It works by appending "_={timestamp}" to the GET parameters. The parameter is not needed for other types of requests.<br>Default is `true`
* | **complete** | *function* | A function to be called when the request finishes (after `success` and `error` callbacks are executed). The function gets passed two arguments: The XMLHTTPRequest object and a string with the status of the request.
* | **data** | *string* / *object* | Data to be sent to the server. It is converted to a query string, if not already a string. It's appended to the url for GET requests. Object must be `key/value` pairs, where `value` can also be an array.
* | **error** | *function* | A function to be called if the request fails. The function receives two arguments: The XMLHttpRequest object and a string describing the type of error that occurred.
* | **method** | *string* | The HTTP method to use for the request (e.g. `POST`, `GET`, `PUT`).
* | **success** | *function* | A function to be called if the request succeeds. The function gets passed two arguments: the data returned from the server and a string describing the status.
*
*
* @memberof ZebraJS
* @alias $.ajax
* @instance
*/
$.ajax = function(url, options) {
const defaults = {
async: true,
beforeSend: null,
cache: true,
complete: null,
data: null,
error: null,
method: 'get',
success: null
};
let httpRequest;
// this callback functions is called as the AJAX call progresses
const callback = function() {
// get the request's status
switch (httpRequest.readyState) {
// if the request is ready to be made
case 1:
// if we have a callback function ready to handle this event, call it now
if (typeof options.beforeSend === 'function') options.beforeSend.call(null, httpRequest, options);
break;
// if the request completed
case 4:
// HTTP success status codes are in the 2xx range (200-299)
// also treat "304 Not Modified" as success (cached content is valid)
const is_success = (httpRequest.status >= 200 && httpRequest.status < 300) || httpRequest.status === 304;
// if the request was successful and we have a callback function ready to handle this situation
if (is_success && typeof options.success === 'function')
// call that function now
options.success.call(null, httpRequest.responseText, httpRequest.status);
// if the request was unsuccessful and we have a callback function ready to handle this situation
else if (!is_success && typeof options.error === 'function')
// call that function now
options.error.call(null, httpRequest.status, httpRequest.responseText);
// if we have a callback function ready to handle the fact that the request completed (regardless if
// it was successful or not)
if (typeof options.complete === 'function')
// call that function now
options.complete.call(null, httpRequest, httpRequest.status);
break;
}
};
// helper function to recursively serialize objects and arrays
const serialize = function(obj, prefix) {
const str = [];
for (const k in obj)
if (obj.hasOwnProperty(k)) {
const v = obj[k];
// build the key - use prefix if available (for nested objects/arrays)
const key_name = prefix ? prefix + '[' + k + ']' : k;
// if value is an object or array, serialize it recursively
if (v !== null && typeof v === 'object' && !v.nodeType) str.push(serialize(v, key_name));
// otherwise, encode the key-value pair
else str.push(encodeURIComponent(key_name) + '=' + encodeURIComponent(v));
}
return str.join('&');
};
// if method is called with a single argument
if (!options) {
// then "options" is actually the first argument
options = url;
// and the "url" is taken from the "options" object
url = options.url;
}
// extend the default options with the ones provided by the user
options = $.extend(defaults, options);
// the method of the request needs to be uppercase
options.method = options.method.toUpperCase();
// if data is provided and is an object
if (options.data && typeof options.data === 'object')
// serialize the data object (handles nested objects and arrays)
options.data = serialize(options.data);
// if we don't want to cache requests, append a query string to the existing ones
if (!options.cache) options.data = (options.data || '') + (options.data ? '&' : '') + '_=' + (+new Date());
// if the XMLHttpRequest object is available
if (window.XMLHttpRequest) {
// instantiate the XMLHttpRequest object
httpRequest = new XMLHttpRequest();
// this will be called as the call progresses
httpRequest.onreadystatechange = callback;
// this makes the call...
httpRequest.open(options.method, url + (options.method === 'GET' && options.data ? '?' + options.data : ''), options.async);
// set the request header
httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
// with any additional parameters, if provided
httpRequest.send(options.data);
}
}
/**
* Iterates over an array or an object, executing a callback function for each item.
*
* For iterating over a set of matched elements, see the {@link ZebraJS#each each()} method.
*
* @param {function} callback The function to execute for each item in the set. The callback function receives two
* arguments: the item's position in the set, called `index` (0-based), and the item.
* The `this` keyword inside the callback function refers to the item.
* <br><br>
* *Returning `FALSE` from the callback function breaks the loop!*
*
* > **This method is here only for compatibility purposes and you shouldn't use it - you should use instead JavaScript's
* native {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach forEach}**
*
* @example
*
* $.each([1, 2, 3, 4], function(index, value) {
* console.log(index + ': ' + value);
* });
*
* const obj = {
* prop1: 'value1',
* prop2: 'value2'
* };
* $.each(obj, function(index, value) {
* console.log(index + ': ' + value);
* });
*
* @return {undefined}
*
* @memberof ZebraJS
* @alias $.each
* @instance
*/
$.each = function(array, callback) {
let key;
// if argument is an array
if (Array.isArray(array) || (array.length !== undefined && typeof array !== 'string')) {
// iterate through the element in the array
for (key = 0; key < array.length; key++)
// apply the callback function
if (callback.call(array[key], key, array[key]) === false) return;
// if argument is an object
} else
// iterate over the object's properties
for (key in array)
// apply the callback function
if (callback.call(array[key], key, array[key]) === false) return;
};
/**
* Merges the properties of two or more objects together into the first object.
*
* @example
*
* // merge the properties of the last 2 objects into the first one
* $.extend({}, {foo: 'baz'}, {bar: 'biz'});
*
* // the result
* // {foo: 'baz', bar: 'biz'}
*
* @param {object} target An object whose properties will be merged with the properties of the additional objects
* passed as arguments to this method.
*
* @return {object} Returns an object with the properties of the object given as first argument merged with the
* properties of additional objects passed as arguments to this method.
*
* @memberof ZebraJS
* @alias $.extend
* @instance
*/
$.extend = function(target) {
let i, property, result;
// if the "assign" method is available, use it
if (Object.assign) return Object.assign.apply(null, [target].concat(Array.from(arguments).slice(1)));
// if the "assign" method is not available
// if converting the target argument to an object fails, throw an error
try { result = Object(target); } catch (e) { throw new TypeError('Cannot convert undefined or null to object'); }
// iterate over the method's arguments
for (i = 1; i < arguments.length; i++)
// if argument is an object
if (typeof arguments[i] === 'object')
// iterate over the object's properties
for (property in arguments[i])
// avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(arguments[i], property))
// add property to the result
result[property] = arguments[i][property];
// return the new object
return result;
}
/**
* Search for a given value within an array and returns the first index where the value is found, or `-1` if the value
* is not found.
*
* This method returns `-1` when it doesn't find a match. If the searched value is in the first position in the array
* this method returns `0`, if in second `1`, and so on.
*
* > Because in JavaScript `0 == false` (but `0 !== false`), to check for the presence of value within array, you need to
* check if it's not equal to (or greater than) `-1`.
* <br><br>
* > **This method is here only for compatibility purposes and you shouldn't use it - you should use instead JavaScript's
* own {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf indexOf}**
*
* @example
*
* // returns 4
* $.inArray(5, [1, 2, 3, 4, 5, 6, 7]);
*
* @param {mixed} value The value to search for
*
* @param {array} array The array to search in
*
* @return {integer} Returns the position of the searched value inside the given array (starting from `0`), or `-1`
* if the value couldn't be found.
*
* @memberof ZebraJS
* @alias $.inArray
* @instance
*/
$.inArray = function(value, array) {
// return the index of "value" in the "array"
return array.indexOf(value);
}
/**
* Determines whether the object given as argument is an array.
*
* > **This method is here only for compatibility purposes and you shouldn't use it - you should use instead JavaScript's
* own {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray Array.isArray}**
*
* @example
*
* // returns TRUE
* $.isArray([1, 2, 3, 4, 5, 6, 7]);
*
* @param {mixed} object Object to test whether or not it is an array.
*
* @return {bool} A boolean indicating whether the object is a JavaScript array (not an array-like object
* such as a ZebraJS object).
*
* @memberof ZebraJS
* @alias $.isArray
* @instance
*/
$.isArray = function(object) {
// returns a boolean indicating whether the object is a JavaScript array
return Array.isArray(object);
}
/**
* Adds one or more classes to each element in the set of matched elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // add a single class
* elements.addClass('foo');
*
* // add multiple classes
* elements.addClass('foo baz');
*
* // chaining
* elements.addClass('foo baz').css('display', 'none');
*
* @param {string} class_names One or more space-separated class names to be added to each element in the
* set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias addClass
* @instance
*/
$.fn.addClass = function(class_names) {
// add class(es) and return the set of matched elements
return this._class('add', class_names);
}
/**
* Inserts content specified by the argument after each element in the set of matched elements.
*
* Both this and the {@link ZebraJS#insertAfter .insertAfter()} method perform the same task, the main difference being
* in the placement of the content and the target. With `.after()`, the selector expression preceding the method is the
* target after which the content is to be inserted. On the other hand, with `.insertAfter()`, the content precedes the
* method and it is the one inserted after the target element.
*
* > Clones of the inserted element will be created after each element in the set of matched elements, except for the last
* one. The original item will be inserted after the last element.
*
* > If the content to be inserted is an element existing on the page, clones of the element will be created after each
* element in the set of matched elements, except for the last one. The original item will be moved (not cloned) after
* the last element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const target = $('#selector');
*
* // insert a div that we create on the fly
* target.after($('<div>').text('hello'));
*
* // same thing as above
* target.after($('<div>hello</div>'));
*
* // inserting elements already existing on the page
* target.after($('ul'));
*
* // insert a string (which will be transformed in HTML)
* target.after('<div>hello</div>');
*
* // chaining
* target.append($('div')).addClass('foo');
*
* @param {mixed} content DOM element, text node, HTML string or ZebraJS object to be inserted after each
* element in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias after
* @instance
*/
$.fn.after = function(content) {
// call the "_dom_insert" private method with these arguments
return this._dom_insert(content, 'after');
}
/**
* Perform a custom animation of a set of CSS properties using transitions.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // fade out
* elements.animate({
* opacity: 0
* }, 250, () => {
* console.log('Animation is complete!');
* });
*
* @param {object} properties An object of CSS properties and values that the animation will move toward.
*
* @param {number|string} [duration] A string or a number determining how long, in milliseconds, the animation will run.
* <br><br>
* The strings `'fast'` and `'slow'` may also be supplied to indicate durations of
* `200` and `600` milliseconds, respectively.
* <br><br>
* Default is `400`
*
* @param {string} [easing] The easing function to use.
* <br><br>
* An easing function specifies the speed at which the animation progresses at
* different points within the animation.
* <br><br>
* Allowed values are:
* <ul>
* <li>ease</li>
* <li>ease-in</li>
* <li>ease-in-out</li>
* <li>ease-out</li>
* <li>linear</li>
* <li>swing</li>
* <li>{@link https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function#the_cubic-bezier_class_of_easing_functions cubic-bezier(...)} (see {@link https://easings.net/ this} for some examples)</li>
* </ul>
* Default is `swing`
*
* > This argument may be skipped!
*
* @param {function} [callback] A function to call once the animation is complete, called once per matched element.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias animate
* @instance
*/
$.fn.animate = function(properties, duration, easing, callback) {
const animation_duration = (duration === 'fast' ? 200 : (duration === 'slow' ? 600 : (typeof duration === 'number' && duration >= 0 ? duration : 400))) / 1000;
let animation_easing = typeof easing === 'string' ? (['ease', 'ease-in', 'ease-in-out', 'ease-out', 'linear', 'swing'].includes(easing) || easing.match(/cubic\-bezier\(.*?\)/g) ? easing : 'swing') : 'swing';
const elements_data = [];
// apply formulas for these easing
if (animation_easing === 'linear') animation_easing = 'cubic-bezier(0.0, 0.0, 1.0, 1.0)';
else if (animation_easing === 'swing') animation_easing = 'cubic-bezier(.02, .01, .47, 1)';
// if the "easing" argument is skipped
if (typeof easing === 'function') callback = easing;
// batch all style reads to minimize reflows
this.forEach(element => {
elements_data.push({
element: element,
styles: window.getComputedStyle(element)
});
});
// batch all style writes
elements_data.forEach(data => {
let property, animation_data;
let cleanup_done = false;
const final_properties = {};
// cleanup function that handles both transitionend and timeout scenarios
const cleanup = function(e) {
// prevent double execution
if (cleanup_done) return;
cleanup_done = true;
// clear the timeout if it exists
if (timeout) clearTimeout(timeout);
// cleanup - remove transition property so future CSS changes don't animate unexpectedly
data.element.style.transition = '';
// clean up animation data
// (this is used in case .stop() is called on the element)
if ($._data_storage) {
animation_data = $._data_storage.get(data.element);
// if we have this data set
if (animation_data) {
// unset these values
animation_data.zjs_animating = false;
animation_data.zjs_animation_properties = null;
animation_data.zjs_animation_cleanup = null;
animation_data.zjs_animation_timeout = null;
}
}
// call user callback if provided
if (callback) callback.call(data.element, e);
};
// initialize WeakMap storage if needed
if (!$._data_storage) $._data_storage = new WeakMap();
// get data object for this element
animation_data = $._data_storage.get(data.element);
// if no data yet
if (!animation_data) {
// initialize and store now
animation_data = {};
$._data_storage.set(data.element, animation_data);
}
// prepare final properties object with units added
for (property in properties)
final_properties[property] = properties[property] + (!isNaN(properties[property]) && !unitless_properties.includes(property) ? 'px' : '');
// store animation state for .stop() method
animation_data.zjs_animating = true;
animation_data.zjs_animation_properties = final_properties;
animation_data.zjs_animation_cleanup = cleanup;
// explicitly set the current values of the properties we are about to animate
for (property in properties)
data.element.style[property] = data.styles[property];
// listen for transition end to clean up and call callback
$(data.element).one('transitionend', cleanup);
// set a timeout fallback in case transitionend never fires
// (element removed from DOM, display:none, no actual transition, etc.)
const timeout = setTimeout(cleanup, (animation_duration * 1000) + 50);
// store timeout reference for .stop() method
animation_data.zjs_animation_timeout = timeout;
// set the transition property
data.element.style.transition = 'all ' + animation_duration + 's ' + animation_easing;
// set the final values of the properties we are about to animate
for (property in final_properties)
data.element.style[property] = final_properties[property];
});
return this;
}
/**
* Inserts content, specified by the argument, to the end of each element in the set of matched elements.
*
* Both this and the {@link ZebraJS#appendTo .appendTo()} method perform the same task, the main difference being in the
* placement of the content and the target. With `.append()`, the selector expression preceding the method is the
* container into which the content is to be inserted. On the other hand, with `.appendTo()`, the content precedes the
* method, and it is inserted into the target container.
*
* > If there is more than one target element, clones of the inserted element will be created for each target except for
* the last one. For the last target, the original item will be inserted.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created for
* each target except for the last one. For the last target, the original item will be moved (not cloned).
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const parent = $('#selector');
*
* // append a div that we create on the fly
* parent.append($('<div>').text('hello'));
*
* // same thing as above
* parent.append($('<div>hello</div>'));
*
* // append one or more elements that already exist on the page
* // if "parent" is a single element than the list will be moved inside the parent element
* // if "parent" is a collection of elements, clones of the list element will be created for
* // each target except for the last one; for the last target, the original list will be moved
* parent.append($('ul'));
*
* // append a string (which will be transformed in HTML)
* // this is more efficient memory wise
* parent.append('<div>hello</div>');
*
* // chaining
* parent.append($('div')).addClass('foo');
*
* @param {mixed} content DOM element, text node, HTML string or ZebraJS object to insert at the end of each
* element in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements (the parents, not the appended elements).
*
* @memberof ZebraJS
* @alias append
* @instance
*/
$.fn.append = function(content) {
// call the "_dom_insert" private method with these arguments
return this._dom_insert(content, 'append');
}
/**
* Inserts every element in the set of matched elements to the end of the parent element(s), specified by the argument.
*
* Both this and the {@link ZebraJS#append .append()} method perform the same task, the main difference being in the
* placement of the content and the target. With `.append()`, the selector expression preceding the method is the
* container into which the content is to be inserted. On the other hand, with `.appendTo()`, the content precedes the
* method, and it is inserted into the target container.
*
* > If there is more than one target element, clones of the inserted element will be created for each target except for
* the last one. For the last target, the original item will be inserted.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created for
* each target except for the last one. For the last target, the original item will be moved (not cloned).
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const parent = $('#selector');
*
* // append a div that we create on the fly
* $('<div>').text('hello').appendTo(parent);
*
* // same thing as above
* $('<div>hello</div>').appendTo(parent);
*
* // append one or more elements that already exist on the page
* // if "parent" is a single element than the list will be moved inside the parent element
* // if "parent" is a collection of elements, clones of the list element will be created for
* // each target except for the last one; for the last target, the original list will be moved
* $('ul').appendTo(parent);
*
* @param {ZebraJS} parent A ZebraJS object at end of which to insert each element in the set of matched elements.
*
* @return {ZebraJS} Returns the ZebraJS object you are appending to.
*
* @memberof ZebraJS
* @alias appendTo
* @instance
*/
$.fn.appendTo = function(parent) {
// call the "_dom_insert" private method with these arguments
return $(parent)._dom_insert(this, 'append');
}
/**
* Gets the value of an attribute for the first element in the set of matched elements, or sets one or more attributes
* for every matched element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // get the value of an attribute for the first
* // element in the set of matched elements
* elements.attr('id');
*
* // set a single attribute
* elements.attr('title', 'title');
*
* // set multiple attributes
* elements.attr({
* title: 'title',
* href: 'href'
* });
*
* // remove an attribute
* elements.attr('title', false);
*
* // chaining
* elements.attr('title', 'title').removeClass('foo');
*
* @param {string|object} attribute If given as a `string` representing an attribute and `value` **is not** set, this
* method will return that particular attribute's value for the first element in the
* set of matched elements.
* <br><br>
* If given as a `string` representing an attribute and `value` **is** set, this
* method will set that particular attribute's value for all the elements in the
* set of matched elements.
* <br><br>
* If given as an `object`, this method will set the given attributes to the given
* values for all the elements in the set of matched elements.
*
* @param {string} [value] The value to be set for the attribute given as argument. *Only used if `attribute`
* is not an object!*
* <br><br>
* Setting it to `false` or `null` will instead **remove** the attribute from the
* set of matched elements.
*
* @return {ZebraJS|mixed} When `setting` attributes, this method returns the set of matched elements.
* When `reading` attributes, this method returns the value of the required attribute.
*
* @memberof ZebraJS
* @alias attr
* @instance
*/
$.fn.attr = function(attribute, value) {
// if attribute argument is an object
if (typeof attribute === 'object')
// iterate over the set of matched elements
this.forEach(element => {
// iterate over the attributes
for (const i in attribute)
// set each attribute
element.setAttribute(i, attribute[i]);
});
// if attribute argument is a string
else if (typeof attribute === 'string')
// if the value argument is provided
if (undefined !== value)
// iterate over the set of matched elements
this.forEach(element => {
// if value argument's value is FALSE or NULL
if (value === false || value === null)
// remove the attribute
element.removeAttribute(attribute);
// for other values, set the attribute's property
else element.setAttribute(attribute, value);
});
// if the value argument is not provided
else
// return the value of the requested attribute
// of the first element in the set of matched elements
// (return "undefined" in case of an empty selection)
return this[0] ? this[0].getAttribute(attribute) : undefined;
// if we get this far, return the set of matched elements
return this;
}
/**
* Inserts content, specified by the argument, before each element in the set of matched elements.
*
* Both this and the {@link ZebraJS#insertBefore .insertBefore()} method perform the same task, the main difference
* being in the placement of the content and the target. With `.before()`, the selector expression preceding the method
* is the target before which the content is to be inserted. On the other hand, with `.insertBefore()`, the content
* precedes the method, and it is the one inserted before the target element.
*
* > If there is more than one target element, clones of the inserted element will be created before each target except
* for the last one. The original item will be inserted before the last target.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created
* before each target except for the last one. The original item will be moved (not cloned) before the last target.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const target = $('#selector');
*
* // insert a div that we create on the fly
* target.before($('<div>').text('hello'));
*
* // same thing as above
* target.before($('<div>hello</div>'));
*
* // use one or more elements that already exist on the page
* // if "target" is a single element than the list will be moved before the target element
* // if "parent" is a collection of elements, clones of the list element will be created before
* // each target, except for the last one; the original list will be moved before the last target
* target.before($('ul'));
*
* // insert a string (which will be transformed in HTML)
* // this is more efficient memory wise
* target.append('<div>hello</div>');
*
* // chaining
* target.append($('div')).addClass('foo');
*
* @param {mixed} content DOM element, text node, HTML string, or {@link ZebraJS} object to be inserted before
* each element in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements (the parents, not the inserted elements).
*
* @memberof ZebraJS
* @alias before
* @instance
*/
$.fn.before = function(content) {
// call the "_dom_insert" private method with these arguments
return this._dom_insert(content, 'before');
}
/**
* Gets the children of each element in the set of matched elements, optionally filtered by a selector.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get all the element's children
* const children_all = element.children();
*
* // get all the "div" children of the element
* const children_filtered = element.children('div');
*
* // chaining
* element.children('div').addClass('foo');
*
* @param {string} selector If the selector is supplied, the elements will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns the children of each element in the set of matched elements, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias children
* @instance
*/
$.fn.children = function(selector) {
// get the children of each element in the set of matched elements, optionally filtered by a selector
return this._add_prev_object(this._dom_search('children', selector));
}
/**
* Creates a deep copy of the set of matched elements.
*
* This method performs a deep copy of the set of matched elements meaning that it copies the matched elements as well
* as all of their descendant elements and text nodes.
*
* Normally, any event handlers bound to the original element are not copied to the clone. Setting the `with_data_and_events`
* argument to `true` will copy the event handlers and element data bound to the original element.
*
* > This method may lead to duplicate element IDs in a document. Where possible, it is recommended to avoid cloning
* elements with this attribute or using class attributes as identifiers instead.
*
* Element data will be shallow-copied when `with_data_and_events` is `true`. This means objects, arrays, and functions
* will be shared between the original and clone. To deep copy data, copy each property manually.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // clone element with data and events, including data and events of children
* const clones = element.clone(true, true)
*
* // chaining - clone and insert into the body element
* element.clone(true, true).appendTo($('body'));
*
* @param {boolean} with_data_and_events Setting this argument to `true` will instruct the method to also copy
* event handlers and element data along with the elements.
*
* @param {boolean} deep_with_data_and_events Setting this argument to `true` will instruct the method to also copy
* event handlers and data for all children of the cloned element.
*
* @return {ZebraJS} Returns the cloned elements, as a {@link ZebraJS} object.
*
* @memberof ZebraJS
* @alias clone
* @instance
*/
$.fn.clone = function(with_data_and_events, deep_with_data_and_events) {
const result = [];
const $this = this;
// iterate over the set of matched elements
this.forEach(element => {
// clone the element (together with its children)
const clone = element.cloneNode(true);
// add to array
result.push(clone);
// if events and data needs to be cloned too
if (with_data_and_events) {
// iterate over all the existing event listeners
event_listeners.forEach((listeners, event_type) => {
// iterate over the events of current type
listeners.forEach(properties => {
// if this is an event attached to element we've just cloned
if (with_data_and_events && properties[0] === element)
// also add the event to the clone element
$(clone).on(event_type + (properties[2] ? '.' + properties[2] : ''), properties[1]);
});
});
// if WeakMap storage has been initialized
if ($._data_storage) {
// do we have complex objects stored for the element?
const element_data = $._data_storage.get(element);
// if we do
if (element_data) {
// create a shallow copy of the data object
// objects, arrays, and functions are shared (not deep cloned)
const cloned_data = {};
for (const key in element_data)
cloned_data[key] = element_data[key];
// store the cloned data for the cloned element
$._data_storage.set(clone, cloned_data);
}
}
}
// if event handlers and data for all children of the cloned element should be also copied
if (deep_with_data_and_events) $this._clone_data_and_events(element, clone);
});
// return the clone elements
return $(result);
}
/**
* For each element in the set, get the first element that matches the selector by traversing up through its ancestors
* in the DOM tree, beginning with the current element.
*
* Given a {@link ZebraJS} object that represents a set of DOM elements, this method searches through the ancestors of
* these elements in the DOM tree, beginning with the current element, and constructs a new {@link ZebraJS} object from
* the matching elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the first parent that is a div
* const closest = element.closest('div');
*
* // chaining
* element.closest('div').addClass('foo');
*
* @param {string} selector If the selector is supplied, the parents will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns zero or one element for each element in the original set, as a {@link ZebraJS} object
*
* @memberof ZebraJS
* @alias closest
* @instance
*/
$.fn.closest = function(selector) {
const result = [];
// since the checking starts with the element itself, if the element itself matches the selector return now
if (this[0] && _query(selector, this[0], 'matches')) return this;
// iterate through the set of matched elements
this.forEach(element => {
// unless we got to the root of the DOM, get the element's parent
while (!((element = element.parentNode) instanceof Document))
// if selector was specified and element matches it, don't look any further
if (_query(selector, element, 'matches')) {
// if not already in the array, add parent to the results array
if (!result.includes(element)) result.push(element);
// don't look any further
break;
}
});
// return the matched elements, as a ZebraJS object
return this._add_prev_object($(result));
}
/**
* Gets the value of a computed style property for the first element in the set of matched elements, or sets one or more
* CSS properties for every matched element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // get the value of a computed style property
* // for the first element in the set of matched elements
* elements.css('width');
*
* // set a single CSS property
* elements.css('position', 'absolute');
*
* // set multiple CSS properties
* elements.css({
* position: 'absolute',
* left: 0,
* top: 0
* });
*
* // remove a property
* elements.attr('position', false);
*
* // chaining
* elements.css('position', 'absolute').removeClass('foo');
*
* @param {string|object} property If given as a `string` representing a CSS property and `value` **is not** set,
* this method will return the computed style of that particular property for the
* first element in the set of matched elements.
* <br><br>
* If given as a `string` representing a CSS property and `value` **is** set, this
* method will set that particular CSS property's value for all the elements in the
* set of matched elements.
* <br><br>
* If given as an `object`, this method will set the given CSS properties to the
* given values for all the elements in the set of matched elements.
*
* @param {string} [value] The value to be set for the CSS property given as argument. *Only used if `property`
* is not an object!*
* <br><br>
* Setting it to `false` or `null` will instead **remove** the CSS property from the
* set of matched elements.
*
* @return {ZebraJS|mixed} When `setting` CSS properties, this method returns the set of matched elements.
* When `reading` CSS properties, this method returns the value(s) of the required computed style(s).
*
* @memberof ZebraJS
* @alias css
* @instance
*/
$.fn.css = function(property, value) {
// if "property" is an object and "value" is not set
if (typeof property === 'object')
// iterate through the set of matched elements
this.forEach(element => {
// iterate through the "properties" object
for (const i in property)
// set each style property
element.style[i] = property[i] +
// if value does not have a unit provided and is not one of the unitless properties, add the "px" suffix
(parseFloat(property[i]) === property[i] && !unitless_properties.includes(i) ? 'px' : '');
});
// if "property" is not an object, and "value" argument is set
else if (undefined !== value)
// iterate through the set of matched elements
this.forEach(element => {
// if value argument's value is FALSE or NULL
if (value === false || value === null)
// remove the CSS property
element.style[property] = '';
// set the respective style property
else element.style[property] = value;
});
// if "property" is not an object and "value" is not set
// return the value of the given CSS property, or "undefined" if property is not available
else {
// return "undefined" in case of an empty selection
if (!this[0]) return undefined;
// get the first element's computed styles
const computedStyle = window.getComputedStyle(this[0]);
// return the sought property's value
return computedStyle[property];
}
// if we get this far, return the matched elements
return this;
}
/**
* Stores arbitrary data associated with the matched elements, or returns the value at the named data store for the
* first element in the set of matched elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // set some data
* elements.data('foo', 'baz');
*
* // retrieve previously set data
* elements.data('foo');
*
* // set an object as data
* elements.data('foo', {bar: 'baz', qux: 2});
*
* @param {string} name A string naming the piece of data to set.
*
* @param {mixed} value The value to associate with the data set.
*
* @return {ZebraJS|mixed} When `setting` data attributes, this method returns the set of matched elements.
* When `reading` data attributes, this method returns the stored values, or `undefined`
* if not data found for the requested key.
*
* @memberof ZebraJS
* @alias data
* @instance
*/
$.fn.data = function(name, value) {
// WeakMap for storing complex objects (DOM elements, array-like object with DOM elements, functions , etc.)
// use a shared WeakMap attached to the $ object to persist across calls
if (!$._data_storage) $._data_storage = new WeakMap();
// if no name is given, return "undefined"
if (undefined === name) return undefined;
// make sure the name follows the Dataset API specs
// http://www.w3.org/TR/html5/dom.html#dom-dataset
name = name
// replace "-" followed by an ascii letter to that letter in uppercase
.replace(/\-([a-z])/ig, (match, letter) => letter.toUpperCase())
// remove any left "-"
.replace(/\-/g, '');
// if "value" argument is provided
if (undefined !== value) {
// iterate through the set of matched elements
this.forEach(element => {
// check if value is a complex object that can't be JSON stringified properly
// (functions, DOM elements, objects with methods)
if (typeof value === 'function' || (typeof value === 'object' && value !== null && (
// DOM element
value.nodeType ||
// array-like object with at least one DOM element
(value.length !== undefined && Array.from(value).some(item => item && item.nodeType))
))) {
// try to get the existing WeakMap data for the element
let element_data = $._data_storage.get(element);
// if WeakMap data is not yet initialized
if (!element_data) {
// initialize it now and set it
element_data = {};
$._data_storage.set(element, element_data);
}
// add/update entry with the requested value
element_data[name] = value;
// for non-complex objects
} else
try {
// use dataset for simple values
element.dataset[name] = typeof value === 'object' ? JSON.stringify(value) : value;
// if JSON.stringify fails (e.g., circular reference), fall back to WeakMap
} catch (e) {
let element_data = $._data_storage.get(element);
if (!element_data) {
element_data = {};
$._data_storage.set(element, element_data);
}
element_data[name] = value;
}
});
// return the set of matched elements, for chaining
return this;
}
// make sure we return "undefined" if the next code block doesn't yield a result
value = undefined;
// if we are retrieving a data value
// iterate through the set of matched elements
this.some(element => {
// first check if we have any data in the WeakMap associated with the element
const element_data = $._data_storage.get(element);
// if we do
if (element_data && undefined !== element_data[name]) {
// extract it
value = element_data[name];
// don't look further
return true;
}
// then check dataset for simple values
if (element.dataset && undefined !== element.dataset[name]) {
// first
try {
// check if the stored value is a JSON object
// if it is, convert it back to an object
value = JSON.parse(element.dataset[name]);
// if the stored value is not a JSON object
} catch (e) {
// get value
value = element.dataset[name];
}
// break out of the loop
return true;
}
});
// return the found value
// (or "undefined" if not found)
return value;
}
/**
* Removes the set of matched elements from the DOM.
*
* This method is the same as the {@link ZebraJS#remove .remove()} method, except that .detach() keeps all events and
* data associated with the removed elements. This method is useful when removed elements are to be reinserted into the
* DOM at a later time.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // remove elements from the DOM
* const detached = element.detach();
*
* // add them back, together with data and events,
* // to the end of the body element
* $('body').insert(detached);
*
* @return {ZebraJS} Returns the removed elements.
*
* @memberof ZebraJS
* @alias detach
* @instance
*/
$.fn.detach = function() {
let result = [];
// iterate over the set of matched elements
this.forEach(element => {
// the ZebraJS object
const $element = $(element);
// clone the element (deep with data and events and add it to the results array)
result = result.concat($element.clone(true, true));
// remove the original element from the DOM
$element.remove();
});
// return the removed elements
return $(result);
}
/**
* Iterates over the set of matched elements, executing a callback function for each element in the set.
*
* @param {function} callback The function to execute for each item in the set. The callback function receives two
* arguments: the element's position in the set, called `index` (0-based), and the DOM
* element. The `this` keyword inside the callback function refers to the DOM element.
* <br><br>
* *Returning `FALSE` from the callback function breaks the loop!*
*
* @example
*
* $('selector').each(function(index) {
*
* // show the element's index in the set
* console.log(index);
*
* // remember, inside the callback, the "this" keyword refers to the DOM element
* $(this).css('display', 'none');
*
* });
*
* @return {void}
*
* @memberof ZebraJS
* @alias each
* @instance
*/
$.fn.each = function(callback) {
// iterate through the set of matched elements
for (let i = 0; i < this.length; i++)
// apply the callback function
if (callback.call(this[i], i, this[i]) === false) return;
}
/**
* Ends the most recent filtering operation in the current chain and returns the set of matched elements to its previous state.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // find spans, add a class to them, then go back to the original selection
* element.find('span')
* .addClass('highlight')
* .end()
* .addClass('container');
*
* // this is useful for traversing and then returning to the previous set
* $('div')
* .children('.child')
* .addClass('selected')
* .end()
* .addClass('parent');
*
* @return {ZebraJS} Returns the previous set of matched elements, or an empty set if there is no previous set.
*
* @memberof ZebraJS
* @alias end
* @instance
*/
$.fn.end = function() {
// return the previous object if it exists, otherwise return an empty ZebraJS object
return this.prevObject || $([]);
}
/**
* Reduces the set of matched elements to the one at the specified index.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('.selector');
*
* // assuming there are 6 elements in the set of matched elements
* // add the "foo" class to the 5th element
* elements.eq(4).addClass('foo');
*
* @param {integer} index An integer indicating the 0-based position of the element. If a negative integer is
* given the counting will go backwards, starting from the last element in the set.
*
* @return {ZebraJS} Returns the element at the specified index, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias eq
* @instance
*/
$.fn.eq = function(index) {
// return the element at the specified index
return this._add_prev_object($(this.get(index)));
}
/**
* Gets the descendants of each element in the current set of matched elements, filtered by a selector, {@link ZebraJS}
* object, or a DOM element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // find the element's div descendants
* const target = element.find('div');
*
* // this is equivalent with the above
* const target2 = $('div', element);
*
* // chaining
* element.find('div').addClass('foo');
*
* @param {string} selector A selector to filter descendant elements by. It can be a query selector, a
* {@link ZebraJS} object, or a DOM element.
*
* @return {ZebraJS} Returns the descendants of each element in the current set of matched elements, filtered by a
* selector, {@link ZebraJS} object, or DOM element, as a {@link ZebraJS} object.
*
* @memberof ZebraJS
* @alias find
* @instance
*/
$.fn.find = function(selector) {
let result = [];
// iterate through the set of matched elements
this.forEach(element => {
// if selector is a ZebraJS object
if (typeof selector === 'object' && selector.version)
// iterate through the elements in the object
selector.forEach(wrapped => {
// if the elements are the same, add it to the results array
if (wrapped.isSameNode(element)) result.push(element);
});
// selector is the Document object, a DOM node, the Window object
else if (typeof selector === 'object' && (selector instanceof Document || selector instanceof Element || selector instanceof Window)) {
// if the elements are the same, add it to the results array
if (selector.isSameNode(element)) result.push(element);
// selector is a string
// get the descendants of the element that match the selector, and add them to the results array
} else result.push(..._query(selector, element));
});
// when it finds no elements, "querySelector" returns "null"
// we'll filter those out now
result = result.filter(entry => entry !== null);
// return the resulting array as a ZebraJS object
return this._add_prev_object($(result));
}
/**
* Constructs a new {@link ZebraJS} object from the first element in the set of matched elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // returns the first element from the list of matched elements, as a ZebraJS object
* const first = elements.first();
*
* @return {ZebraJS} Returns the first element from the list of matched elements, as a ZebraJS object
*
* @memberof ZebraJS
* @alias first
* @instance
*/
$.fn.first = function() {
// returns the first element from the list of matched elements, as a ZebraJS object
return this._add_prev_object(this[0] ? $(this[0]) : $());
}
/**
* Retrieves one of the elements matched by the {@link ZebraJS} object.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // this gets the second DOM element from the list of matched elements
* elements.get(1);
*
* @param {integer} index The index (starting from `0`) of the DOM element to return from the list of matched
* elements
*
* @memberof ZebraJS
* @alias get
* @instance
*/
$.fn.get = function(index) {
// handle negative indexes
if (index < 0) index = Math.max(0, this.length + index);
// return the matching DOM element
return undefined !== this[index] ? this[index] : undefined;
}
/**
* Checks whether *any* of the matched elements have the given class.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // check if matched elements have a certain class
* const class_exists = elements.hasClass('foo');
*
* // chaining
* elements.toggleClass('foo');
*
* @param {string} class_name The name of a class to be checked if it exists on *any* of the elements in the set
* of matched elements.
*
* @return {boolean} Returns TRUE if the sought class exists in *any* of the elements in the set of matched elements.
*
* @memberof ZebraJS
* @alias hasClass
* @instance
*/
$.fn.hasClass = function(class_name) {
// return TRUE if any element has the class
return this.some(element => element.classList.contains(class_name));
}
/**
* Returns the content height (without `padding`, `border` and `margin`) of the first element in the set of matched
* elements as `float`, or sets the `height` CSS property of every element in the set.
*
* See {@link ZebraJS#outerHeight .outerHeight()} for getting the height including `padding`, `border` and, optionally,
* `margin`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // returns the content height of the first element in the set of matched elements
* elements.height();
*
* // sets the "height" CSS property of all elements in the set to 200px
* elements.height(200);
* elements.height('200');
* elements.height('200px');
*
* // sets the "height" CSS property of all elements in the set to 5em
* elements.height('5em');
*
* // chaining
* elements.height(200).addClass('foo');
*
* @param {undefined|number|string} [height] If not given, this method will return content height (without `padding`,
* `border` and `margin`) of the first element in the set of matched
* elements.
* <br><br>
* If given, this method will set the `height` CSS property of all
* the elements in the set to that particular value, making sure
* to apply the "px" suffix if not otherwise specified.
*
* > For hidden elements the returned value is `0`!
*
* @return {ZebraJS|float} When **setting** the `height`, this method returns the set of matched elements. Otherwise,
* it returns the content height (without `padding`, `border` and `margin`) of the first
* element in the set of matched elements, as `float`.
*
* @memberof ZebraJS
* @alias height
* @instance
*/
$.fn.height = function(height) {
// if "height" is given, set the height of every matched element, making sure to suffix the value with "px"
// if not otherwise specified
if (height !== undefined) return this.css('height', height + (parseFloat(height) === height ? 'px' : ''));
// for the "window"
if (this[0] === window) return window.innerHeight;
// for the "document"
if (this[0] === document)
// return height
return Math.max(
document.body.offsetHeight,
document.body.scrollHeight,
document.documentElement.clientHeight,
document.documentElement.offsetHeight,
document.documentElement.scrollHeight
);
// get the first element's height, top/bottom padding and borders
const styles = window.getComputedStyle(this[0]);
const offset_height = this[0].offsetHeight;
const border_top_width = parseFloat(styles.borderTopWidth);
const border_bottom_width = parseFloat(styles.borderBottomWidth);
const padding_top = parseFloat(styles.paddingTop);
const padding_bottom = parseFloat(styles.paddingBottom);
// return height
return offset_height - border_bottom_width - border_top_width - padding_top - padding_bottom;
}
/**
* Hides an element from the DOM by settings its "display" property to `none`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('selector');
*
* // hide the element from the DOM
* element.hide();
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias hide
* @instance
*/
$.fn.hide = function() {
// iterate through the set of matched elements
this.forEach(element => {
// set the "display" property
element.style.display = 'none';
});
// return the set of matched elements
return this;
}
/**
* Gets the HTML content of the first element in the set of matched elements, or set the HTML content of every matched
* element.
*
* > There are some {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#Security_considerations security considerations}
* that you should be aware of when using this method.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // set the HTML content for all the matched elements
* elements.html('<p>Hello</p>');
*
* // get the HTML content of the first
* // element in the set of matched elements
* const content = elements.html();
*
* // chaining
* elements.html('<p>Hello</p>').addClass('foo');
* @param {string} [content] The HTML content to set as the content of all the matched elements. Note that any
* content that was previously in that element is completely replaced by the new
* content.
*
* @return {ZebraJS|string} When the `content` argument is provided, this method returns the set of matched
* elements. Otherwise it returns the HTML content of the first element in the set of
* matched elements.
*
* @memberof ZebraJS
* @alias html
* @instance
*/
$.fn.html = function(content) {
// if content is provided
if (undefined !== content)
// iterate through the set of matched elements
this.forEach(element => {
// set the HTML content of each element
element.innerHTML = content;
});
// if content is not provided
// return the content of the first element in the set of matched elements
else return this[0] ? this[0].innerHTML : undefined;
// return the set of matched elements
return this;
}
/**
* Returns the position of an element among its siblings or within a set of elements.
*
* - If no argument is passed, the method returns an integer indicating the position of the first element within
* the {@link ZebraJS} object relative to its sibling elements.
*
* - If a selector is passed as an argument, the return value is an integer indicating the position of the first
* element within the {@link ZebraJS} object relative to the elements matched by the selector. If the element is
* not found, it returns `-1`.
*
* - If a DOM element or {@link ZebraJS} object is passed, the method returns an integer indicating the position
* of that element relative to the elements in the original {@link ZebraJS} object.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('li');
*
* // get the index of the first element among its siblings
* let index = elements.index();
*
* // get the index of the first element within a selector
* index = elements.index('.active');
*
* // get the index of a specific element within the set
* const element = $('li.active');
* index = elements.index(element);
*
* @param {mixed} [selector] Can be a selector, a DOM element, or a {@link ZebraJS} object.
*
* @return {number} Returns the index of the element, or `-1` if not found.
*
* @memberof ZebraJS
* @alias index
* @instance
*/
$.fn.index = function(selector) {
// if no argument is provided
if (undefined === selector) {
// if we have elements
if (this.length > 0) {
// get the first element
const element = this[0];
// get all child elements of the parent (or empty array if no parent)
const elements = element.parentNode ? Array.from(element.parentNode.children) : [];
// find and return the index (returns -1 if empty array)
return elements.indexOf(element);
}
// if no elements, return -1
return -1;
}
// if a selector string is provided
if (typeof selector === 'string') {
// if we have elements
if (this.length > 0) {
// get the first element
const element = this[0];
// get all elements matching the selector
const elements = _query(selector, document);
// find and return the index
return elements.indexOf(element);
}
// if no elements, return -1
return -1;
}
// if a DOM element is provided
if (selector instanceof Element)
// find the index of this element in the current set
return this.findIndex(el => el === selector);
// if a ZebraJS object is provided
if (typeof selector === 'object' && selector.version) {
// if the object has elements
if (selector.length > 0)
// find the index of the first element in the current set
return this.findIndex(el => el === selector[0]);
// not found
return -1;
}
// default return
return -1;
}
/**
* Checks the current matched set of elements against a selector, element or ZebraJS object and returns `true` if at
* least one of these elements matches the given arguments.
*
* Supports both standard CSS selectors and jQuery filtering pseudo-selectors like `:visible`, `:hidden`, `:has()`,
* `:contains()`, `:header`, `:input`, etc.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // returns true if the element is a "select" element
* console.log(element.is('select'))
*
* @param {mixed} selector A string containing a selector expression to match elements against, a DOM element
* or a ZebraJS object.
*
* @return {boolean} Returns `true` if at least one of the elements from the currently matched set matches the given
* argument.
*
* @memberof ZebraJS
* @alias is
* @instance
*/
$.fn.is = function(selector) {
let result = false;
// iterate over the set of matched elements
this.forEach(element => {
// if
if (
// selector is a CSS selector and the current element matches the selector OR
(typeof selector === 'string' && _query(selector, element, 'matches')) ||
// selector is a ZebraJS object and the current element matches the first element in the set of matched elements OR
(typeof selector === 'object' && selector.version && element === selector[0]) ||
// selector is a DOM element and current element matches it
(typeof selector === 'object' && (selector instanceof Document || selector instanceof Element || selector instanceof Text || selector instanceof Window) && element === selector)
) {
// set result to TRUE
result = true;
// don't look further
return false;
}
});
// return result
return result;
}
/**
* Inserts every element in the set of matched elements after the parent element(s), specified by the argument.
*
* Both this and the {@link ZebraJS#after .after()} method perform the same task, the main difference being in the
* placement of the content and the target. With `.after()`, the selector expression preceding the method is the target
* after which the content is to be inserted. On the other hand, with `.insertAfter()`, the content precedes the method,
* and it is the one inserted after the target element(s).
*
* > If there is more than one target element, clones of the inserted element will be created after each target except
* for the last one. The original item will be inserted after the last target.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created
* after each target except for the last one. The original item will be moved (not cloned) after the last target.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const target = $('#selector');
*
* // insert a div that we create on the fly
* $('<div>').text('hello').insertAfter(target);
*
* // same thing as above
* $('<div>hello</div>').insertAfter(target);
*
* // use one or more elements that already exist on the page
* // if "target" is a single element than the list will be moved after the target element
* // if "parent" is a collection of elements, clones of the list element will be created after
* // each target, except for the last one; the original list will be moved after the last target
* $('ul').insertAfter(target);
*
* @param {ZebraJS} target A ZebraJS object after which to insert each element in the set of matched elements.
*
* @return {ZebraJS} Returns the ZebraJS object after the content is inserted.
*
* @memberof ZebraJS
* @alias insertAfter
* @instance
*/
$.fn.insertAfter = function(target) {
// call the "_dom_insert" private method with these arguments
return $(target)._dom_insert(this, 'after');
}
/**
* Inserts every element in the set of matched elements before the parent element(s), specified by the argument.
*
* Both this and the {@link ZebraJS#before .before()} method perform the same task, the main difference being in the
* placement of the content and the target. With `.before()`, the selector expression preceding the method is the target
* before which the content is to be inserted. On the other hand, with `.insertBefore()`, the content precedes the method,
* and it is the one inserted before the target element(s).
*
* > If there is more than one target element, clones of the inserted element will be created before each target except
* for the last one. The original item will be inserted before the last target.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created
* before each target except for the last one. The original item will be moved (not cloned) before the last target.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const target = $('#selector');
*
* // insert a div that we create on the fly
* $('<div>').text('hello').insertBefore(target);
*
* // same thing as above
* $('<div>hello</div>').insertBefore(target);
*
* // use one or more elements that already exist on the page
* // if "target" is a single element than the list will be moved before the target element
* // if "parent" is a collection of elements, clones of the list element will be created before
* // each target, except for the last one; the original list will be moved before the last target
* $('ul').insertBefore(target);
*
* @param {ZebraJS} target A ZebraJS object before which to insert each element in the set of matched elements.
*
* @return {ZebraJS} Returns the ZebraJS object before which the content is inserted.
*
* @memberof ZebraJS
* @alias insertBefore
* @instance
*/
$.fn.insertBefore = function(target) {
// call the "_dom_insert" private method with these arguments
return $(target)._dom_insert(this, 'before');
}
/**
* Gets the immediately following sibling of each element in the set of matched elements. If a selector is provided,
* it retrieves the following sibling only if it matches that selector.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the next sibling
* const next = element.next();
*
* // get the following sibling only if it matches the selector
* const next2 = element.next('div');
*
* // chaining
* element.next().addClass('foo');
*
* @param {string} selector If the selector is provided, the method will retrieve the following sibling only if
* it matches the selector
*
* @return {ZebraJS} Returns the immediately following sibling of each element in the set of matched elements,
* optionally filtered by a selector, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias next
* @instance
*/
$.fn.next = function(selector) {
// get the immediately preceding sibling of each element in the set of matched elements,
// optionally filtered by a selector
return this._add_prev_object(this._dom_search('next', selector));
}
/**
* Removes elements from the set of matched elements.
*
* @example
*
* // find all elements having class ".foo" but not ".bar"
* $('.foo').not('.bar');
*
* @param {mixed} selector Can be a **string** containing a selector expression, a **DOM** element, an **array
* of elements** to match against the set, or a **function** used as a test for each
* element in the set.
* <br><br>
* If argument is a function, it accepts two arguments: "index", which is the element's
* index in the set of matched elements, and "element", which is the DOM element.<br>
* Within the function, `this` refers to the current DOM element.
*
* @memberof ZebraJS
* @alias not
* @instance
*/
$.fn.not = function(selector) {
// iterate over the set of matched elements
return this._add_prev_object($(this.filter(function(element, index) {
// if selector is a function, use it to filter results
if (typeof selector === 'function' && selector.call !== undefined) return selector.call(element, index);
// if selector is an array of elements
if (Array.isArray(selector))
// filter results
return !selector.filter(current_selector => $(element).is(current_selector)).length;
// otherwise use "is" to filter results
return !$(element).is(selector);
})));
}
/**
* Remove an event handler.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // create a function
* const callback = e => {
* console.log('clicked!');
* }
*
* // handle clicks on element using the function created above
* element.on('click', callback);
*
* // remove that particular click event
* element.off('click', callback);
*
* // remove *all* the click events
* element.off('click');
*
* // remove *only* the click events that were namespaced
* element.off('click.namespace');
*
* @param {string} event_type One or more space-separated event types and optional namespaces, such as "click" or
* "click.namespace".
*
* @param {function} callback A function to execute when the event is triggered.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias off
* @instance
*/
$.fn.off = function(event_type, callback) {
const event_types = event_type ? event_type.split(' ') : [...event_listeners.keys()];
const remove_all_event_handlers = !event_type;
let namespace, index, entry, actual_event_type;
// iterate through the set of matched elements
this.forEach(element => {
// iterate through the event types we have to remove the handler from
event_types.forEach(original_event => {
// handle namespacing
namespace = original_event.split('.')
actual_event_type = namespace[0];
namespace = namespace[1] || '';
// if we have registered event of this type
if (event_listeners.has(actual_event_type)) {
const listeners = event_listeners.get(actual_event_type);
// iterate through the registered events of this type
// we're going backwards to avoid memory leaks while iterating through an array while
// simultaneously splicing from it
for (index = listeners.length - 1; index >= 0; index--) {
entry = listeners[index];
// if
if (
// this is an event registered for the current element
entry[0] === element &&
// no callback was specified (we need to remove all events of this type) OR
// callback is given and we've just found it
(undefined === callback || callback === entry[1]) &&
// we're looking at the right namespace (or we need to remove all event handlers)
(remove_all_event_handlers || namespace === entry[2])
) {
// remove the event listener
element.removeEventListener(actual_event_type, entry[3] || entry[1]);
// remove entry from the event listeners array
listeners.splice(index, 1);
// if nothing left for this event type then also remove the event type's entry
if (listeners.length === 0) event_listeners.delete(actual_event_type);
// don't look further
return;
}
}
}
});
});
// return the set of matched elements, for chaining
return this;
}
/**
* Gets the current coordinates of the first element in the set of matched elements, relative to the document.
*
* This method retrieves the current position of an element relative to the document, in contrast with the
* {@link ZebraJS#position .position()} method which retrieves the current position relative to the offset parent.
*
* > This method cannot get the position of hidden elements or accounting for borders, margins, or padding set on the
* body element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the element's position, relative to the offset parent
* const offset = element.offset()
*
* @return {object} Returns an object with the `left` and `top` properties.
*
* @memberof ZebraJS
* @alias offset
* @instance
*/
$.fn.offset = function() {
// return now in case of an empty selection
if (!this[0]) return {
left: 0,
top: 0
};
// get the bounding box of the first element in the set of matched elements
const box = this[0].getBoundingClientRect();
// return the object with the offset
return {
left: box.left + window.pageXOffset - document.documentElement.clientLeft,
top: box.top + window.pageYOffset - document.documentElement.clientTop
}
}
/**
* Attaches an event handler function for one or more events to the selected elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // create a function
* const callback = e => {
* console.log('clicked!');
* }
*
* // handle clicks on element using the function created above
* element.on('click', callback);
*
* // handle clicks on element using an anonymous function
* element.on('click', e => {
* console.log('clicked!');
* });
*
* // namespacing, so that you can remove only certain events
* element.on('click.namespace', e => {
* console.log('clicked!');
* });
*
* // passing data to the event handler
* element.on('click', { foo: 'bar' }, e => {
* console.log(e.data.foo); // 'bar'
* });
*
* // using delegation
* // handle clicks on all the "div" elements
* // that are children of the element
* element.on('click', 'div', e => {
* console.log('clicked!');
* });
*
* // using delegation with data
* element.on('click', 'div', { userId: 123 }, e => {
* console.log(e.data.userId); // 123
* });
*
* // chaining
* element.on('click', () => {
* console.log('clicked!');
* }).addClass('foo');
*
* // multiple events
* element.on({
* mouseenter: () => { ... },
* mouseleave: () => { ... }
* });
*
* @param {string} event_type One or more space-separated event types and optional namespaces, such as "click" or
* "click.namespace". Can also be given as an object.
*
* @param {string} [selector] A selector string to filter the descendants of the selected elements that will call
* the handler. If the selector is null or omitted, the handler is always called when it
* reaches the selected element.
*
* @param {object} [data] Data to be passed to the handler in `event.data` when an event is triggered.
*
* @param {function} callback A function to execute when the event is triggered.
*
* @param {boolean} [once] Whether the callback should be called a single time only
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias on
* @instance
*/
$.fn.on = function(event_type, selector, data, callback, once) {
let namespace, actual_callback, event_data;
// if event_type is given as object
if (typeof event_type === 'object') {
// iterate over all the events
for (const i in event_type)
// bind them
this.on(i, event_type[i]);
// don't go forward
return this;
}
// if more than a single event was given
const event_types = event_type.split(' ');
// handle optional selector and data
// case 1: selector is a function - on(event_type, callback)
if (typeof selector === 'function') {
// shift parameters
if (typeof data === 'boolean') once = data;
callback = selector;
selector = undefined;
data = undefined;
// case 2: selector is an object - on(event_type, data, callback)
} else if (typeof selector === 'object' && selector !== null && !Array.isArray(selector)) {
// shift parameters
event_data = selector;
if (typeof callback === 'boolean') once = callback;
if (typeof data === 'function') callback = data;
selector = undefined;
data = undefined;
// case 3: selector is a string
} else if (typeof selector === 'string')
// if data is a function - on(event_type, selector, callback)
if (typeof data === 'function') {
if (typeof callback === 'boolean') once = callback;
callback = data;
data = undefined;
// if data is an object - on(event_type, selector, data, callback)
} else if (typeof data === 'object' && data !== null && !Array.isArray(data))
event_data = data;
// iterate through the set of matched elements
this.forEach(element => {
// iterate through the event types we have to attach the handler to
event_types.forEach(original_event => {
actual_callback = false;
// handle namespacing
namespace = original_event.split('.')
event_type = namespace[0];
namespace = namespace[1] || '';
// if this is the first time we have this event type
if (!event_listeners.has(event_type))
// initialize the entry for this event type
event_listeners.set(event_type, []);
// if selector is a string
if (typeof selector === 'string') {
// this will be the actual callback function
actual_callback = function(e) {
// attach data to event object if provided
if (event_data) e.data = event_data;
// if the callback needs to be executed only once, remove it now
if (once) $(this).off(original_event, callback);
// walk up the DOM tree from e.target to "this" (the element the listener is attached to)
// to find an element that matches the selector
let target = e.target;
// as long as we didn't yet find the element the listener is attached to
while (target && target !== this) {
// if the element matches the selector
if (_query(selector, target, 'matches')) {
// call the callback with the matched element as 'this'
callback.call(target, e);
// stop after first match (don't continue bubbling up)
return;
}
// continue walking up the DOM tree
target = target.parentNode;
}
};
// attach event listener
element.addEventListener(event_type, actual_callback);
// if the callback needs to be executed only once
} else if (once) {
// the actual callback function
actual_callback = function(e) {
// attach data to event object if provided
if (event_data) e.data = event_data;
// remove the event handler
$(this).off(original_event, callback);
// execute the callback function
callback.call(this, e);
}
// set the event listener
element.addEventListener(event_type, actual_callback);
// registering of default event listeners
} else
// if we have event data, wrap the callback
if (event_data) {
actual_callback = function(e) {
// attach data to event object
e.data = event_data;
// execute the callback function
callback.call(this, e);
};
element.addEventListener(event_type, actual_callback);
// no event data, register callback directly
} else element.addEventListener(event_type, callback);
// add element/callback combination to the array of events of this type
event_listeners.get(event_type).push([element, callback, namespace, actual_callback]);
});
});
// return the set of matched elements, for chaining
return this;
}
/**
* Attaches an event handler function for one or more events to the selected elements. The event handler is executed at
* most once per element per event type.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // create a function
* const callback = function(e) {
* console.log('clicked!');
* }
*
* // handle clicks on element using the function created above
* // (the callback will be executed only once)
* element.one('click', callback);
*
* // handle clicks on element using an anonymous function
* // (the callback will be executed only once)
* element.one('click', function(e) {
* console.log('clicked!');
* });
*
* // namespacing, so that you can remove only certain events
* // (the callback will be executed only once)
* element.one('click.namespace', function(e) {
* console.log('clicked!');
* });
*
* // using delegation
* // handle clicks on all the "div" elements
* // that are children of the element
* // (the callback will be executed only once for each matched element)
* element.one('click', 'div', function(e) {
* console.log('clicked!');
* });
*
* // chaining
* element.one('click', function() {
* console.log('clicked!');
* }).addClass('foo');
*
* @param {string} event_type One or more space-separated event types and optional namespaces, such as "click" or
* "click.namespace".
*
* @param {string} [selector] A selector string to filter the descendants of the selected elements that will call
* the handler. If the selector is null or omitted, the handler is always called when it
* reaches the selected element.
*
* @param {function} callback A function to execute when the event is triggered.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias one
* @instance
*/
$.fn.one = function(event_type, selector, callback) {
// call the "on" method with last argument set to TRUE
return this.on(event_type, selector, callback, true);
}
/**
* Returns the height (including `padding`, `border` and, optionally, `margin`) for the first element in the set of
* matched elements.
*
* > For hidden elements the returned value is `0`!
*
* See {@link ZebraJS#height .height()} for getting the **inner** height without `padding`, `border` and `margin`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('selector');
*
* // get the element's outer height
* const height = element.outerHeight();
*
* @param {boolean} [include_margins] If set to `TRUE`, the result will also include **top** and **bottom**
* margins.
*
* @return {float}
*
* @memberof ZebraJS
* @alias outerHeight
* @instance
*/
$.fn.outerHeight = function(include_margins) {
// get computed styles only if we need margins
const computed_styles = include_margins ? window.getComputedStyle(this[0]) : null;
// return outer height (content + padding + border)
return this[0].offsetHeight +
// include margins, if requested
(include_margins ? parseFloat(computed_styles.marginTop) + parseFloat(computed_styles.marginBottom) : 0);
}
/**
* Returns the width (including `padding`, `border` and, optionally, `margin`) for the first element in the set of
* matched elements.
*
* > For hidden elements the returned value is `0`!
*
* See {@link ZebraJS#width .width()} for getting the **inner** width without `padding`, `border` and `margin`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('selector');
*
* // get the element's outer width
* const height = element.outerWidth();
*
* @param {boolean} [include_margins] If set to `TRUE`, the result will also include **left** and **right**
* margins.
*
* @return {float}
*
* @memberof ZebraJS
* @alias outerWidth
* @instance
*/
$.fn.outerWidth = function(include_margins) {
// get computed styles only if we need to include margins
const computed_styles = include_margins ? window.getComputedStyle(this[0]) : null;
// return outer width (content + padding + border)
return this[0].offsetWidth +
// include margins, if requested
(include_margins ? parseFloat(computed_styles.marginLeft) + parseFloat(computed_styles.marginRight) : 0);
}
/**
* Gets the immediate parent of each element in the current set of matched elements, optionally filtered by a selector.
*
* This method is similar to {@link ZebraJS#parents .parents()}, except .parent() only travels a single level up the
* DOM tree.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the element's parent
* const parent = element.parent();
*
* // get the element's parent *only* if it is a div
* const parent2 = element.parent('div');
*
* // chaining
* element.parent().addClass('foo');
*
* @param {string} selector If the selector is supplied, the elements will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns the immediate parent of each element in the current set of matched elements, optionally
* filtered by a selector, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias parent
* @instance
*/
$.fn.parent = function(selector) {
const result = [];
// iterate through the set of matched elements
this.forEach(element => {
// if not a detached element, no selector is provided or it is and the parent matches it, add element to the array
if (element.parentNode && (!selector || _query(selector, element.parentNode, 'matches'))) result.push(element.parentNode);
});
// return the resulting array
return this._add_prev_object($(result));
}
/**
* Gets the ancestors of each element in the current set of matched elements, optionally filtered by a selector.
*
* Given a {@link ZebraJS} object that represents a set of DOM elements, this method allows us to search through the
* ancestors of these elements in the DOM tree and construct a new {@link ZebraJS} object from the matching elements
* ordered from immediate parent on up; the elements are returned in order from the closest parent to the outer ones.
* When multiple DOM elements are in the original set, the resulting set will have duplicates removed.
*
* This method is similar to {@link ZebraJS#parent .parent()}, except .parent() only travels a single level up the DOM
* tree, while this method travels all the way up to the DOM root.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get *all* the element's parent
* const parents = element.parents();
*
* // get all the element's parent until the first div (including also that first div)
* const parents2 = element.parents('div');
*
* // chaining
* element.parents('div').addClass('foo');
*
* @param {string} selector If the selector is supplied, the parents will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns an array of parents of each element in the current set of matched elements, optionally
* filtered by a selector, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias parents
* @instance
*/
$.fn.parents = function(selector) {
const result = [];
// iterate through the set of matched elements
this.forEach(element => {
// unless we got to the root of the DOM, get the element's parent
while (!((element = element.parentNode) instanceof Document)) {
// if not already in the array, add parent to the results array
if (!result.includes(element)) result.push(element);
// if selector was specified and element matches it, don't look any further
if (selector && _query(selector, element, 'matches')) break;
}
});
// return the matched elements, as a ZebraJS object
return this._add_prev_object($(result));
}
/**
* Gets the current coordinates of the first element in the set of matched elements, relative to the offset parent.
*
* This method retrieves the current position of an element relative to the offset parent, in contrast with the
* {@link ZebraJS#offset .offset()} method which retrieves the current position relative to the document.
*
* > This method cannot get the position of hidden elements or accounting for borders, margins, or padding set on the
* body element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the element's position, relative to the offset parent
* const position = element.position()
*
* @return {object} Returns an object with the `left` and `top` properties.
*
* @memberof ZebraJS
* @alias position
* @instance
*/
$.fn.position = function() {
// return now in case of an empty selection
if (!this[0]) return {
left: 0,
top: 0
};
// return the position of the first element in the set of matched elements
return {
left: parseFloat(this[0].offsetLeft),
top: parseFloat(this[0].offsetTop)
}
}
/**
* Inserts content, specified by the argument, to the beginning of each element in the set of matched elements.
*
* Both this and the {@link ZebraJS#prependTo .prependTo()} method perform the same task, the main difference being in
* the placement of the content and the target. With `.prepend()`, the selector expression preceding the method is the
* container into which the content is to be inserted. On the other hand, with `.prependTo()`, the content precedes the
* method, and it is inserted into the target container.
*
* > If there is more than one target element, clones of the inserted element will be created for each target except for
* the last one. For the last target, the original item will be inserted.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created for
* each target except for the last one. For the last target, the original item will be moved (not cloned).
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const parent = $('#selector');
*
* // append a div that we create on the fly
* parent.prepend($('<div>').text('hello'));
*
* // same thing as above
* parent.prepend($('<div>hello</div>'));
*
* // prepend one or more elements that already exist on the page
* // if "parent" is a single element than the list will be moved inside the parent element
* // if "parent" is a collection of elements, clones of the list element will be created for
* // each target except for the last one; for the last target, the original list will be moved
* parent.prepend($('ul'));
*
* // prepend a string (which will be transformed in HTML)
* // this is more efficient memory wise
* parent.prepend('<div>hello</div>');
*
* // chaining
* parent.prepend($('div')).addClass('foo');
*
* @param {mixed} content DOM element, text node, HTML string, or {@link ZebraJS} object to insert at the
* beginning of each element in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements (the parents, not the prepended elements).
*
* @memberof ZebraJS
* @alias prepend
* @instance
*/
$.fn.prepend = function(content) {
// call the "_dom_insert" private method with these arguments
return this._dom_insert(content, 'prepend');
}
/**
* Inserts every element in the set of matched elements to the beginning of the parent element(s), specified by the argument.
*
* Both this and the {@link ZebraJS#prepend .prepend()} method perform the same task, the main difference being in the
* placement of the content and the target. With `.prepend()`, the selector expression preceding the method is the
* container into which the content is to be inserted. On the other hand, with `.prependTo()`, the content precedes the
* method, and it is inserted into the target container.
*
* > If there is more than one target element, clones of the inserted element will be created for each target except for
* the last one. For the last target, the original item will be inserted.
*
* > If an element selected this way is inserted elsewhere in the DOM, clones of the inserted element will be created for
* each target except for the last one. For the last target, the original item will be moved (not cloned).
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const parent = $('#selector');
*
* // prepend a div that we create on the fly
* $('<div>').text('hello').prependTo(parent);
*
* // same thing as above
* $('<div>hello</div>').prependTo(parent);
*
* // prepend one or more elements that already exist on the page
* // if "parent" is a single element than the list will be moved inside the parent element
* // if "parent" is a collection of elements, clones of the list element will be created for
* // each target except for the last one; for the last target, the original list will be moved
* $('ul').appendTo(parent);
*
* @param {ZebraJS} parent A ZebraJS object at beginning of which to insert each element in the set of matched elements.
*
* @return {ZebraJS} Returns the ZebraJS object you are appending to.
*
* @memberof ZebraJS
* @alias prependTo
* @instance
*/
$.fn.prependTo = function(parent) {
// call the "_dom_insert" private method with these arguments
return $(parent)._dom_insert(this, 'prepend');
}
/**
* Gets the immediately preceding sibling of each element in the set of matched elements. If a selector is provided,
* it retrieves the previous sibling only if it matches that selector.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get the previous sibling
* const prev = element.prev();
*
* // get the previous sibling only if it matches the selector
* const prev2 = element.prev('div');
*
* // since this method returns a ZebraJS object
* element.prev().addClass('foo');
*
* @param {string} selector If the selector is provided, the method will retrieve the previous sibling only if
* it matches the selector
*
* @return {ZebraJS} Returns the immediately preceding sibling of each element in the set of matched elements,
* optionally filtered by a selector, as a ZebraJS object.
*
* @memberof ZebraJS
* @alias prev
* @instance
*/
$.fn.prev = function(selector) {
// get the immediately preceding sibling of each element in the set of matched elements,
// optionally filtered by a selector
return this._add_prev_object(this._dom_search('previous', selector));
}
/**
* Specifies a function to execute when the DOM is fully loaded.
*
* @example
*
* $(document).ready(function() {
* // code to be executed when the DOM is ready
* });
*
* @param {function} callback A function to execute when the DOM is ready and safe to manipulate.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias ready
* @instance
*/
$.fn.ready = function(callback) {
// if DOM is already ready, fire the callback now
if (document.readyState === 'complete' || document.readyState !== 'loading') callback();
// otherwise, wait for the DOM and execute the callback when the it is ready
else document.addEventListener('DOMContentLoaded', callback);
// return the set of matched elements
return this;
}
/**
* Removes the set of matched elements from the DOM.
*
* Use this method when you want to remove the element itself, as well as everything inside it. In addition to the elements
* themselves, all attached event handlers and data attributes associated with the elements are also removed.
*
* To remove the elements without removing data and event handlers, use {@link ZebraJS#detach() .detach()} instead.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // remove the element, its children, and all attached event
* // handlers and data attributes associated with the elements
* element.remove();
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias remove
* @instance
*/
$.fn.remove = function() {
// iterate over the set of matched elements
this.forEach(element => {
// the element as a ZebraJS object
const $element = $(element);
// the element's children
const children = Array.from(element.querySelectorAll('*'));
// iterate over the element's children
children.forEach(child => {
// the child's ZebraJS form
let $child = $(child);
// remove all event handlers
$child.off();
// nullify the child to free memory
$child = null;
});
// remove all attached event handlers
$element.off();
// remove element from the DOM (including children)
if (element.parentNode) element.parentNode.removeChild(element);
});
// return the set of matched elements
return this;
}
/**
* Removes a previously-stored piece of data from the matched elements.
*
* The `.removeData()` method allows us to remove values that were previously set using `.data()`. When called with
* the name of a key, it removes that particular value. When called without any arguments, it removes all data.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // set some data
* element.data('foo', 'bar');
* element.data('baz', {key: 'value'});
*
* // remove specific data
* element.removeData('foo');
*
* // remove all data
* element.removeData();
*
* @param {string} [name] A string naming the piece of data to remove. If omitted, all data will be removed.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias removeData
* @instance
*/
$.fn.removeData = function(name) {
// iterate through the set of matched elements
this.forEach(element => {
// if a specific name is provided
if (undefined !== name) {
// make sure the name follows the Dataset API specs
// http://www.w3.org/TR/html5/dom.html#dom-dataset
name = name
// replace "-" followed by an ascii letter to that letter in uppercase
.replace(/\-([a-z])/ig, (match, letter) => letter.toUpperCase())
// remove any left "-"
.replace(/\-/g, '');
// try to remove from WeakMap storage
if ($._data_storage) {
const element_data = $._data_storage.get(element);
// if we have data for this element
if (element_data && element_data[name] !== undefined)
// remove the specific property
delete element_data[name];
}
// try to remove from dataset
if (element.dataset && element.dataset[name] !== undefined)
// remove the data attribute
delete element.dataset[name];
// if no name is provided, remove all data
} else {
// remove all WeakMap data for this element
if ($._data_storage) $._data_storage.delete(element);
// remove all dataset attributes
if (element.dataset) {
// get all data attribute names
const keys = Object.keys(element.dataset);
// remove each one
keys.forEach(key => {
delete element.dataset[key];
});
}
}
});
// return the set of matched elements, for chaining
return this;
}
/**
* Removes one or more classes from each element in the set of matched elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // remove a single class
* elements.removeClass('foo');
*
* // remove multiple classes
* elements.removeClass('foo baz');
*
* // since this method returns the set of matched elements
* elements.removeClass('foo baz').css('display', 'none');
*
* @param {string} class_names One or more space-separated class names to be removed from each element in
* the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias removeClass
* @instance
*/
$.fn.removeClass = function(class_names) {
// remove class(es) and return the set of matched elements
return this._class('remove', class_names);
}
/**
* Replaces each element in the set of matched elements with the provided new content and returns the set of elements
* that was removed.
*
* > Note that if the method's argument is a selector, then clones of the element described by the selector will be
* created and used for replacing each element in the set of matched elements, except for the last one. The original
* item will be moved (not cloned) and used to replace the last target.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // wrap element in a div
* element.replaceWith('<div id="replacement"></div>');
*
* // *exactly* the same thing as above
* element.replaceWith($('<div id="replacement"></div>'));
*
* // using an existing element as the wrapper
* element.replaceWith($('#element-from-the-page'));
*
* @param {mixed} element A string, a {@link ZebraJS} object or a DOM element to use as replacement for each
* element in the set of matched elements.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias replaceWith
* @instance
*/
$.fn.replaceWith = function(element) {
// call the "_dom_insert" private method with these arguments
return this._dom_insert(element, 'replace');
}
/**
* Gets the horizontal position of the scrollbar for the first element in the set of matched elements, or sets the
* horizontal position of the scrollbar for every matched element.
*
* The horizontal scroll position is the same as the number of pixels that are hidden from view above the scrollable area.
* If the scroll bar is at the very left, or if the element is not scrollable, this number will be `0`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const body = $('body');
*
* // get the horizontal scroll of the body
* body.scrollLeft();
*
* // set the horizontal scroll of the body
* body.scrollLeft(250);
*
* // chaining
* elements.scrollLeft(250).addClass('foo');
*
* @param {integer} [value] Sets the horizontal position of the scrollbar for every matched element.
*
* @return {ZebraJS|integer} When `setting` the horizontal position, this method returns the set of matched elements.
* When `reading` the horizontal position, this method returns the horizontal position of
* the scrollbar for the first element in the set of matched elements.
*
* @memberof ZebraJS
* @alias scrollLeft
* @instance
*/
$.fn.scrollLeft = function(value) {
// if value is not specified, return the scrollLeft value of the first element in the set of matched elements
if (undefined === value) return this[0] instanceof Window || this[0] instanceof Document ? document.documentElement.scrollLeft : this[0].scrollLeft;
// iterate through the set of matched elements
this.forEach(element => {
// set the scrollLeft value for each element
// apply "parseFloat" in case is provided as string or suffixed with "px"
element.scrollLeft = parseFloat(value);
});
// return the matched elements
return this;
}
/**
* Gets the vertical position of the scrollbar for the first element in the set of matched elements, or sets the
* vertical position of the scrollbar for every matched element.
*
* The vertical scroll position is the same as the number of pixels that are hidden from view above the scrollable area.
* If the scroll bar is at the very top, or if the element is not scrollable, this number will be `0`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const body = $('body');
*
* // get the vertical scroll of the body
* body.scrollTop();
*
* // set the vertical scroll of the body
* body.scrollTop(250);
*
* // chaining
* elements.scrollTop(250).addClass('foo');
*
* @param {integer} [value] Sets the vertical position of the scrollbar for every matched element.
*
* @return {ZebraJS|integer} When `setting` the vertical position, this method returns the set of matched elements.
* When `reading` the vertical position, this method returns the vertical position of
* the scrollbar for the first element in the set of matched elements.
*
* @memberof ZebraJS
* @alias scrollTop
* @instance
*/
$.fn.scrollTop = function(value) {
// if value is not specified, return the scrollTop value of the first element in the set of matched elements
if (undefined === value) return this[0] instanceof Window || this[0] instanceof Document ? document.documentElement.scrollTop : this[0].scrollTop;
// iterate through the set of matched elements
this.forEach(element => {
// set the scrollTop value for each element
// apply "parseFloat" in case is provided as string or suffixed with "px"
element.scrollTop = parseFloat(value);
});
// return the matched elements
return this;
}
/**
* If the first element in the set of matched elements is a `form` element, this method returns the encodes string of
* the form's elements and their respective values.
*
* > Only "successful controls" are serialized to the string. No submit button value is serialized since the form was
* not submitted using a button. For a form element's value to be included in the serialized string, the element must
* have a name attribute. Values from checkboxes and radio buttons (inputs of type "radio" or "checkbox") are included
* only if they are checked. Data from file select elements is not serialized. Image inputs (`type="image"`) are
* serialized by their value attribute if present, but click coordinates (.x/.y) are not included since this is
* programmatic serialization, not an actual form submission.
*
* This method creates a text string in standard URL-encoded notation.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const form = $('#form');
*
* // serialize form's elements and their values
* const serialized = form.serialize();
*
* @return {string} Returns the serialized form as a query string that could be sent to a server in an Ajax request.
*
* @memberof ZebraJS
* @alias serialize
* @instance
*/
$.fn.serialize = function() {
// return quickly if an empty selection
if (!this[0]) return '';
const form = this[0];
const result = [];
// if element is a form
if (typeof form === 'object' && form.nodeName.toUpperCase() === 'FORM')
// iterate over the form's elements
Array.from(form.elements).forEach(control => {
// if element has a name, it is not disabled and it is not a "file", a "reset", a "submit" not a "button"
if (control.name && !control.disabled && !['file', 'reset', 'submit', 'button'].includes(control.type))
// if element is a multiple select
if (control.type === 'select-multiple')
// iterate over the available options
Array.from(control.options).forEach(option => {
// add each selected option to the result
if (option.selected) result.push(encodeURIComponent(control.name) + '=' + encodeURIComponent(option.value))
});
// if not a radio or a checkbox, or a checked radio/checkbox
else if (!['checkbox', 'radio'].includes(control.type) || control.checked)
// add to result
result.push(encodeURIComponent(control.name) + '=' + encodeURIComponent(control.value));
});
// return the serialized result
return result.join('&').replace(/\%20/g, '+');
}
/**
* Sets an element's "display" property to `` (an empty string).
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('selector');
*
* // make element visible in the DOM
* element.show();
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias show
* @instance
*/
$.fn.show = function() {
// iterate through the set of matched elements
this.forEach(element => {
// unset the "display" property
element.style.display = '';
});
// return the set of matched elements
return this;
}
/**
* Stops the currently-running animation on the matched elements.
*
* When `.stop()` is called on an element, the currently-running animation (if any) is immediately stopped. If an
* element is being animated, the animation stops in its current position. If `jump_to_end` is set to `true`, the
* animation will jump to its end position.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('#selector');
*
* // start an animation
* elements.animate({
* left: '+=100px',
* opacity: 0.5
* }, 1000);
*
* // stop the animation at current position
* elements.stop();
*
* // stop and jump to end position
* elements.stop(false, true);
*
* @param {boolean} [clear_queue] This parameter is included for jQuery compatibility but is currently not used
* as ZebraJS does not implement animation queuing. Reserved for future use.
* <br><br>
* Default is `false`
*
* @param {boolean} [jump_to_end] A boolean indicating whether to complete the current animation immediately.
* When `true`, the animation will jump to its end state. When `false` or omitted,
* the animation stops at its current position.
* <br><br>
* Default is `false`
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias stop
* @instance
*/
$.fn.stop = function(clear_queue, jump_to_end) {
// iterate over the set of matched elements
this.forEach(element => {
let property, transition_property, properties_list;
const $element = $(element);
// if for whatever reason we don't have this property initialized stop now
if (!$._data_storage) return;
// get animation data stored by animate() method
const animation_data = $._data_storage.get(element);
// if no animation data found, nothing to stop
if (!animation_data || !animation_data.zjs_animating) return;
// get current computed styles to freeze or jump
const computed_style = window.getComputedStyle(element);
// remove the "transitionend" event listener
if (animation_data.zjs_animation_cleanup) $element.off('transitionend', animation_data.zjs_animation_cleanup);
// clear the timeout fallback
if (animation_data.zjs_animation_timeout) clearTimeout(animation_data.zjs_animation_timeout);
// stop the transition by setting it to 'none'
element.style.transition = 'none';
// if we need to jump_to_end
if (jump_to_end && animation_data.zjs_animation_properties)
// jump to end: apply the target (end) properties
for (property in animation_data.zjs_animation_properties)
element.style[property] = animation_data.zjs_animation_properties[property];
// if we need to "freeze" the animation at current position
// (apply computed values as inline styles)
else {
// get the list of properties being transitioned
transition_property = computed_style.transitionProperty;
// if transition property is set
if (transition_property && transition_property !== 'none') {
// split into individual properties
properties_list = transition_property.split(', ');
// apply current computed values
properties_list.forEach(prop => {
// skip 'all' and 'none' keywords
if (prop !== 'all' && prop !== 'none') {
// convert CSS property name (e.g., 'margin-left') to camelCase (e.g., 'marginLeft')
prop = prop.replace(/\-([a-z])/g, (match, letter) => letter.toUpperCase());
// apply the current computed value
element.style[prop] = computed_style[prop];
}
});
}
}
// force a reflow to apply the changes immediately
void element.offsetHeight;
// reset transition property for future animations
element.style.transition = '';
// clean up animation data
animation_data.zjs_animating = false;
animation_data.zjs_animation_properties = null;
animation_data.zjs_animation_cleanup = null;
animation_data.zjs_animation_timeout = null;
});
return this;
}
/**
* Gets the siblings of each element in the set of matched elements, optionally filtered by a selector.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // get all the siblings of the element
* const siblings_all = element.siblings();
*
* // get all the "div" siblings of the element
* const siblings_filtered = element.siblings('div');
*
* // since this method returns a ZebraJS object
* element.siblings('div').addClass('foo');
*
* @param {string} selector If the selector is supplied, the elements will be filtered by testing whether they
* match it.
*
* @return {ZebraJS} Returns the siblings of each element in the set of matched elements, as a ZebraJS object
*
* @memberof ZebraJS
* @alias siblings
* @instance
*/
$.fn.siblings = function(selector) {
// get the siblings of each element in the set of matched elements, optionally filtered by a selector.
return this._add_prev_object(this._dom_search('siblings', selector));
}
/**
* Gets the text content of the first element in the set of matched elements (combined with the text content of all its
* descendants), or sets the text contents of the matched elements.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // set the text content for all the matched elements
* elements.text('Hello');
*
* // get the text content of the first element in the
* // set of matched elements (including its descendants)
* const content = elements.text();
*
* // chaining
* elements.text('Hello').addClass('foo');
* @param {string} [content] The text to set as the content of all the matched elements. Note that any text
* content that was previously in that element is completely replaced by the new
* content.
*
* @return {ZebraJS|string} When the `content` argument is provided, this method returns the set of matched
* elements. Otherwise it returns the text content of the first element in the set of
* matched elements (combined with the text content of all its descendants)
*
* @memberof ZebraJS
* @alias text
* @instance
*/
$.fn.text = function(content) {
// if content is provided
if (undefined !== content)
// iterate through the set of matched elements
this.forEach(element => {
// set the text content of each element
element.textContent = content;
});
// if content is not provided
// return the text content of the first element in the set of matched elements
// (combined with the text content of all its descendants)
else return this[0] ? this[0].textContent : undefined;
// return the set of matched elements
return this;
}
/**
* Adds or removes one or more classes from each element in the set of matched elements, depending on the presence of
* each class name given as argument.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // set a random class
* elements.addClass('foo');
*
* // toggle classes
* // the result will be that "foo" will be removed from the matched elements while the "baz" will be added
* elements.toggleClass('foo baz');
*
* // chaining
* elements.toggleClass('foo').css('display', 'none');
*
* @param {string} class_names One or more space-separated class names to be toggled for each element in the set of
* matched elements.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias toggleClass
* @instance
*/
$.fn.toggleClass = function(class_names) {
// toggle class(es) and return the set of matched elements
return this._class('toggle', class_names);
}
/**
* Execute all handlers attached to the matched elements for the given event type, in the same order they would be if
* the event were triggered naturally by the user.
*
* `.trigger()`ed events bubble up the DOM tree; an event handler can stop the bubbling by returning `false` from the
* handler or calling the `.stopPropagation()` method on the event object passed into the event.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // handle clicks on element
* element.on('click', function(e) {
*
* // will return "undefined" when element is clicked
* // but will return "baz" when triggered manually
* console.log(e.foo)
*
* });
*
* // manually trigger the click event
* element.trigger('click', {foo: 'baz'});
*
* // chaining
* element.trigger('click', {foo: 'baz'}).addClass('foo');
*
* @param {string} event_type A string containing a JavaScript event type, such as `click` or `submit`.
*
* @param {object} data Additional parameters to pass along to the event handler.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias trigger
* @instance
*/
$.fn.trigger = function(event_type, data) {
// iterate through the set of matched elements
this.forEach(element => {
// create the event
// the event will bubble and it is cancelable
const event = new Event(event_type, {
bubbles: true,
cancelable: true
});
// if data is specified and is an object
if (typeof data === 'object')
// iterate over the object's keys
Object.keys(data).forEach(key => {
// attach them to the event object
event[key] = data[key];
});
// dispatch the event
element.dispatchEvent(event);
});
// return the set of matched elements, for chaining
return this;
}
/**
* Removes the parents of the set of matched elements from the DOM, leaving the matched elements in their place.
*
* This method is effectively the inverse of the {@link ZebraJS#wrap .wrap()} method. The matched elements (and their
* siblings, if any) replace their parents within the DOM structure.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // unwrap the element, whatever its parent may be
* element.unwrap();
*
* // unwrap only if the element's parent is a div
* element.unwrap('div');
*
* @param {string} selector If the selector is supplied, the parent elements will be filtered and the unwrapping
* will occur only they match it.
*
* @return {ZebraJS} Returns the set of matched elements.
*
* @memberof ZebraJS
* @alias unwrap
* @instance
*/
$.fn.unwrap = function(selector) {
// iterate through the set of matched elements
this.forEach(element => {
// get the element's parent, optionally filtered by a selector,
// and replace it with the element
$(element).parent(selector).replaceWith(element);
});
// return the set of matched elements
return this;
}
/**
* Gets the current value of the first element in the set of matched elements or set the value of every matched element.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('selector');
*
* // get the value of the first element in the list of matched elements
* // (if "element" was a select box with multiple selections allowed,
* // the returned value would be an array)
* const value = element.val();
*
* // set the element's value
* element.val('foo');
*
* // setting multiple values for multi-selects and checkboxes
* element.val(['option1', 'option2']);
*
* @param {mixed} [value] A string, a number, or an array of strings corresponding to the value of each matched
* element to set as selected/checked.
*
* @return {ZebraJS|mixed} If setting a value, this method returns the set of matched elements. If called without
* the argument, the method return the current value of the first element in the set of
* matched elements.
*
* @memberof ZebraJS
* @alias val
* @instance
*/
$.fn.val = function(value) {
const result = [];
// if "value" argument is not specified
if (undefined === value) {
// if first element in the list of matched elements is a select box with the "multiple" attribute set
if (this[0] && this[0].tagName.toLowerCase() === 'select' && this[0].multiple) {
// add each selected option to the results array
Array.from(this[0].options).forEach(elem => {
if (elem.selected && !elem.disabled) result.push(elem.value)
});
// return the values of selected options
return result;
}
// for other elements, return the first element's value
return this[0] ? this[0].value : '';
}
// if "value" argument is specified
// iterate through the set of matched elements
this.forEach(element => {
// if value is not an array
if (!Array.isArray(value))
// set the value of of the current element
element.value = value;
// if value is an array, the current element is an checkbox/radio input and its value is in the array
else if (element.tagName.toLowerCase() === 'input' && element.type && (element.type === 'checkbox' || element.type === 'radio') && element.value && value.includes(element.value))
// mark the element as checked
element.checked = true;
// if element is a select box with the "multiple" attribute set
else if (element.tagName.toLowerCase() === 'select' && element.multiple)
// set the "selected" attribute to each matching option
Array.from(element.options).forEach(elem => {
if (value.includes(elem.value)) elem.selected = true;
});
});
// return the set of matched elements
return this;
}
/**
* Returns the content width (without `padding`, `border` and `margin`) of the first element in the set of matched
* elements as `float`, or sets the `width` CSS property of every element in the set.
*
* See {@link ZebraJS#outerWidth .outerWidth()} for getting the width including `padding`, `border` and, optionally,
* `margin`.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const elements = $('selector');
*
* // returns the content width of the first element in the set of matched elements
* elements.width();
*
* // sets the "width" CSS property of all elements in the set to 200px
* elements.width(200);
* elements.width('200');
* elements.width('200px');
*
* // sets the "width" CSS property of all elements in the set to 5em
* elements.width('5em');
*
* // chaining
* elements.width(200).addClass('foo');
*
* @param {undefined|number|string} [width] If not given, this method will return content width (without `padding`,
* `border` and `margin`) of the first element in the set of matched
* elements.
* <br><br>
* If given, this method will set the `width` CSS property of all
* the elements in the set to that particular value, making sure
* to apply the "px" suffix if not otherwise specified.
*
* > For hidden elements the returned value is `0`!
*
* @return {ZebraJS|float} When **setting** the `width`, this method returns the set of matched elements. Otherwise,
* it returns the content width (without `padding`, `border` and `margin`) of the first
* element in the set of matched elements, as `float`.
*
* @memberof ZebraJS
* @alias width
* @instance
*/
$.fn.width = function(width) {
// if "width" is given, set the width of every matched element, making sure to suffix the value with "px"
// if not otherwise specified
if (width !== undefined) return this.css('width', width + (parseFloat(width) === width ? 'px' : ''));
// for the "window"
if (this[0] === window) return window.innerWidth;
// for the "document"
if (this[0] === document)
// return width
return Math.max(
document.body.offsetWidth,
document.body.scrollWidth,
document.documentElement.clientWidth,
document.documentElement.offsetWidth,
document.documentElement.scrollWidth
);
// get the first element's width, left/right padding and borders
const styles = window.getComputedStyle(this[0]);
const offset_width = this[0].offsetWidth;
const border_left_width = parseFloat(styles.borderLeftWidth);
const border_right_width = parseFloat(styles.borderRightWidth);
const padding_left = parseFloat(styles.paddingLeft);
const padding_right = parseFloat(styles.paddingRight);
// return width
return offset_width - border_left_width - border_right_width - padding_left - padding_right;
}
/**
* Wraps an HTML structure around each element in the set of matched elements.
*
* > Note that if the method's argument is a selector then clones of the element described by the selector will be
* created and wrapped around each element in the set of matched elements except for the last one. The original item will
* be moved (not cloned) and wrapped around the last target.
*
* @example
*
* // always cache selectors
* // to avoid DOM scanning over and over again
* const element = $('#selector');
*
* // wrap element in a div
* element.wrap('<div id="container"></div>');
*
* // *exactly* the same thing as above
* element.wrap($('<div id="container"></div>'));
*
* // using an existing element as the wrapper
* element.wrap($('#element-from-the-page'));
*
* @param {mixed} element A string, a {@link ZebraJS} object or a DOM element in which to wrap around each element
* in the set of matched elements.
*
* @return {ZebraJS} Returns the original set of matched elements.
*
* @memberof ZebraJS
* @alias wrap
* @instance
*/
$.fn.wrap = function(element) {
// call the "_dom_insert" private method with these arguments
this._dom_insert(element, 'wrap');
// return the original element(s)
return this;
}
// this is where we make the $ object available globally
window.$ = window.jQuery = $;
})();