UNPKG

strudel

Version:

A front-end framework for back-end powered web.

727 lines (635 loc) 19.3 kB
/* eslint-disable */ const selectors = {}; selectors[/^\.[\w\-]+$/] = function (param) { return document.getElementsByClassName(param.substring(1)); }; selectors[/^\w+$/] = function (param) { return document.getElementsByTagName(param); }; selectors[/^\#[\w\-]+$/] = function (param) { return document.getElementById(param.substring(1)); }; selectors[/^</] = function (param) { return new Element().generate(param); }; /** * Wrapper for query selector * @param {String} selector - CSS selector * @param {Node} context - Node to select from * @returns {NodeList} */ const byCss = (selector, context) => { return (context || document).querySelectorAll(selector); }; /** * Wrapper for byCss * @param {String} selector * @param {Node} context * @returns {NodeList} */ const select = (selector, context) => { selector = selector.replace(/^\s*/, '').replace(/\s*$/, ''); if (context) { return byCss(selector, context); } for (var key in selectors) { context = key.split('/'); if ((new RegExp(context[1], context[2])).test(selector)) { return selectors[key](selector); } } return byCss(selector); }; // Store all of the operations to perform when cloning elements const mirror = { /** * Copy all JavaScript events of source node to destination node. */ events: function (src, dest) { if (!src._e) return; for (var type in src._e) { src._e[type].forEach(function (event) { new Element(dest).on(type, event); }); } }, /** * Copy select input value to its clone. */ select: function (src, dest) { if (new Element(src).is('select')) { dest.value = src.value; } }, /** * Copy textarea input value to its clone */ textarea: function (src, dest) { if (new Element(src).is('textarea')) { dest.value = src.value; } } }; /** * @classdesc Element class used for DOM manipulation * @class */ class Element { /** * @constructor * @param {string} selector - CSS selector * @param {Node} context - Node to wrap into Element * @returns {Element} */ constructor(selector, context) { if (selector instanceof Element) { return selector; } if (typeof selector === 'string') { selector = select(selector, context); } if (selector && selector.nodeName || selector && selector === window) { selector = [selector]; } this._nodes = this.slice(selector); } /** * Returns size of nodes */ get length() { return this._nodes.length; } /** * Extracts structured data from DOM * @param {Function} callback - A callback to be called on each node. Returned value is added to the set * @returns {*} */ array(callback) { let self = this; return this._nodes.reduce(function (list, node, i) { let val; if (callback) { val = callback.call(self, node, i); if (!val) val = false; if (typeof val === 'string') val = new Element(val); if (val instanceof Element) val = val._nodes; } else { val = node.innerHTML; } return list.concat(val !== false ? val : []); }, []); } /** * Create a string from different things * @private */ str(node, i) { return function (arg) { if (typeof arg === 'function') { return arg.call(this, node, i); } return arg.toString(); }; } /** * Check the current matched set of elements against a selector and return true if at least one of these elements matches the given arguments. * @param {selector} selector - A string containing a selector expression to match elements against. * @returns {boolean} */ is(selector) { return this.filter(selector).length > 0; } /** * Reduce the set of matched elements to those that match the selector or pass the function's test. * @param {selector} selector A string containing a selector expression to match elements against. * @returns {Element} */ filter(selector) { let callback = function (node) { node.matches = node.matches || node.msMatchesSelector || node.webkitMatchesSelector; return node.matches(selector || '*'); }; if (typeof selector === 'function') callback = selector; if (selector instanceof Element) { callback = function (node) { return (selector._nodes).indexOf(node) !== -1; }; } return new Element(this._nodes.filter(callback)); } /** * Reduce the set of matched elements to the one at the specified index. * @param {Number} index - An integer indicating the 0-based position of the element. * @returns {Element|boolean} */ eq(index) { return new Element(this._nodes[index]) || false; } /** * Reduce the set of matched elements to the HTMLElement at the specified index. * @param {Number} index - An integer indicating the 0-based position of the element. * @returns {HTMLElement} */ get(index) { return ((index || index === 0) && index <= this._nodes.length) ? this._nodes[index] : this._nodes; } /** * Reduce the set of matched elements to the first in the set. * @returns {HTMLElement} */ first() { return this._nodes[0] || false; } /** * Returns index of a given element * @param {HTMLElement|Element} element * @returns {Number} */ index(element) { const siblings = this.children()._nodes; const node = element instanceof HTMLElement ? element : element.first(); return Array.prototype.indexOf.call(siblings, node); } /** * Converts Arraylike to array * @private */ slice(pseudo) { if (!pseudo || pseudo.length === 0 || typeof pseudo === 'string' || pseudo.toString() === '[object Function]') return []; return pseudo.length ? [].slice.call(pseudo._nodes || pseudo) : [pseudo]; } /** * Removes duplicated nodes * @private */ unique() { return new Element(this._nodes.reduce(function (clean, node) { let isTruthy = node !== null && node !== undefined && node !== false; return (isTruthy && clean.indexOf(node) === -1) ? clean.concat(node) : clean; }, [])); } /** * Get the direct children of all of the nodes with an optional filter * @param [string] selector - Filter what children to get * @returns {Element} */ children(selector) { return this.map(function (node) { return this.slice(node.children); }).filter(selector); } /** * Generates element from htmlString * @private */ generate(html) { if (/^\s*<t(h|r|d)/.test(html)) { return new Element(document.createElement('table')).html(html).children()._nodes; } else if (/^\s*</.test(html)) { return new Element(document.createElement('div')).html(html).children()._nodes; } else { return document.createTextNode(html); } } /** * Normalize the arguments to an array of strings * @private */ args(args, node, i) { if (typeof args === 'function') { args = args(node, i); } if (typeof args !== 'string') { args = this.slice(args).map(this.str(node, i)); } return args.toString().split(/[\s,]+/).filter(function (e) { return e.length; }); } /** * Loops through the nodes and executes callback for each * @param {Function} callback - The function that will be called * @returns {Element} */ each(callback) { this._nodes.forEach(callback.bind(this)); return this; } /** * Loop through the combination of every node and every argument passed * @private */ eacharg(args, callback) { return this.each(function (node, i) { this.args(args, node, i).forEach(function (arg) { callback.call(this, node, arg); }, this); }); } /** * Checks if node exists on a page * @private */ isInPage(node) { return (node === document.body) ? false : document.body.contains(node); } /** * Changes the content of the current instance by running a callback for each Element * @param {Function} callback - A callback that returns an element that are going to be kept * @returns {Element} */ map(callback) { return callback ? new Element(this.array(callback)).unique() : this; } /** * Add texts in specific position * @private */ adjacent(html, data, callback) { if (typeof data === 'number') { if (data === 0) { data = []; } else { data = new Array(data).join().split(',').map(Number.call, Number); } } return this.each(function (node, j) { let fragment = document.createDocumentFragment(); new Element(data || {}).map(function (el, i) { let part = (typeof html === 'function') ? html.call(this, el, i, node, j) : html; if (typeof part === 'string') { return this.generate(part); } return new Element(part); }).each(function (n) { this.isInPage(n) ? fragment.appendChild(new Element(n).clone().first()) : fragment.appendChild(n); }); callback.call(this, node, fragment); }); } /** * Return an array of DOM nodes of a source node and its children. * @private */ getAll(context) { return new Element([context].concat(new Element('*', context)._nodes)); } /** * Deep clone a DOM node and its descendants. * @returns {Element} */ clone() { return this.map(function (node) { var clone = node.cloneNode(true); var dest = this.getAll(clone); this.getAll(node).each(function (src, i) { for (var key in mirror) { mirror[key](src, dest._nodes[i]); } }); return clone; }); } /** * Gets the HTML contents of the first element in a set. * When parameter is provided set the HTML contents of each element in the set. * @param {htmlString} [text] - A string of HTML to set as the content of each matched element * @returns {htmlString|Element} */ html(text) { if (text === undefined) { return this.first().innerHTML || ''; } return this.each(function (node) { node.innerHTML = text; }); } /** * Gets the text contents of the first element in a set. * When parameter is provided set the text contents of each element in the set. * @param {string} [text] - A string to set as the text content of each matched element. * @returns {string|Element} */ text(text) { if (text === undefined) { return this.first().textContent || ''; } return this.each(function (node) { node.textContent = text; }); } /** * Remove the set of matched elements from the DOM. * @returns {Element} */ remove() { return this.each(function (node) { node.parentNode.removeChild(node); }); } /** * Travel the matched elements one node up * @param {selector} CSS Selector * @returns {Element} */ parent(selector) { return this.map(function (node) { return node.parentNode; }).filter(selector); } /** * Find the first ancestor that matches the selector for each node * @param {selector} CSS Selector * @returns {Element} */ closest(selector) { return this.map(function (node) { do { if (new Element(node).is(selector)) { return node; } } while ((node = node.parentNode) && node !== document); }); } /** * Insert content, specified by the parameter, to the end of each element in the set of matched elements * Additional data can be provided, which will be used for populating the html * @param {string|Element} html - Html string or Element * @param [data] * @returns {Element} */ append(html, data) { return this.adjacent(html, data, function (node, fragment) { node.appendChild(fragment); }); } /** * Insert content, specified by the parameter, to the begining of each element in the set of matched elements * Additional data can be provided, which will be used for populating the html * @param {string|Element} html - Html string or Element * @param [data] * @returns {Element} */ prepend(html, data) { return this.adjacent(html, data, function (node, fragment) { node.insertBefore(fragment, node.firstChild); }); } /** * Get the descendants of each element in the current set of matched elements, filtered by a selector. * @param {selector} selector - A string containing a selector expression to match elements against. * @returns {Element} */ find(selector) { return this.map(function (node) { const startsWithImmediateChildrenSelector = selector[0] === '>'; let hadId; if (startsWithImmediateChildrenSelector) { hadId = true; if (!node.id) { hadId = false; node.id = `strudel-${Math.random().toString(36).substr(2, 9)}`; } selector = `#${node.id}${selector}`; } const result = new Element(selector || '*', node); if (startsWithImmediateChildrenSelector && !hadId) { node.removeAttribute('id'); } return result; }); } /** * Adds the specified class(es) to each element in the set of matched elements. * @param {...string} className - Class(es) to be added * @returns {Element} */ addClass(className) { return this.eacharg(arguments, function (el, name) { el.classList.add(name); }); } /** * Toggles the specified class(es) to each element in the set of matched elements. * @param {...string} className - Class(es) to be toggled * @returns {Element} */ toggleClass(className) { return this.eacharg(arguments, function (el, name) { el.classList.toggle(name); }); } /** * Removes the specified class(es) from each element in the set of matched elements. * @param {...string} className - Class(es) to be removed * @returns {Element} */ removeClass(className) { return this.eacharg(arguments, function (el, name) { el.classList.remove(name); }); } /** * Attach event handlers * @param {string} events - Events to attach handlers for - can be space separated or comma separated list, or array of strings * @param {string|Function} cb - Callback or CSS selector * @param [Function] cb2 - Callback when second parameter is a selector * @returns {Element} */ on(events, cb, cb2) { let providedHandler = cb; if (typeof cb === 'string') { let sel = cb; cb = function (e) { let args = arguments; let el = new Element(e.currentTarget); let set = el.is(sel) ? el : el.find(sel); set.each(function (target) { if (target === e.target || target.contains(e.target)) { try { Object.defineProperty(e, 'currentTarget', { get: function () { return target; } }); } catch (err) { } cb2.apply(target, args); } }); }; providedHandler = cb2; } let eventHandler = function (e) { return cb.apply(this, [e].concat(e.detail || [])); }; return this.eacharg(events, function (node, event) { node.addEventListener(event, eventHandler); node._e = node._e || {}; node._e[event] = node._e[event] || []; node._e[event].push({ providedHandler, eventHandler, }); }); } /** * Remove an event handler * @param {string} events * @param {function} handler to be removed */ off(events, handler) { if (events === undefined && handler === undefined) { this.each(function (node) { for (let event in node._e) { node._e[event].forEach(function ({eventHandler}) { node.removeEventListener(event, eventHandler); }) } node._e = {}; }); } return this.eacharg(events, function (node, event) { new Element(node._e ? node._e[event] : []).each(function ({providedHandler, eventHandler}, index) { if(handler) { if (handler === providedHandler) { node.removeEventListener(event, eventHandler); node._e[event] = [ ...node._e[event].slice(0, index), ...node._e[event].slice(index + 1), ]; } } else { node.removeEventListener(event, eventHandler); node._e[event] = []; } }); }); } /** * Execute all handlers attached to the event type * @param {string} events - Event types to be executed * @returns {*} */ trigger(events) { let data = this.slice(arguments).slice(1); return this.eacharg(events, function (node, event) { let ev; let opts = { bubbles: true, cancelable: true, detail: data }; try { ev = new window.CustomEvent(event, opts); } catch (e) { ev = document.createEvent('CustomEvent'); ev.initCustomEvent(event, true, true, data); } node.dispatchEvent(ev); }); } /** * Get the value of an attribute for the first element in the set. * When parameter is provided set the text contents of each element in the set. * @param [string|object] name - Name of the attribute to be retrieved/set. Can be object of attributes/values. * @param [string] value - Value of the attribute to be set. * @returns {string|Element} */ attr(name, value, data) { data = data ? 'data-' : ''; if (value !== undefined) { let nm = name; name = {}; name[nm] = value; } if (typeof name === 'object') { return this.each(function (node) { for (let key in name) { if (name[key] !== null) { node.setAttribute(data + key, name[key]); } else { node.removeAttribute(data + key); } } }); } return this.length ? this.first().getAttribute(data + name) : ''; } /** * Get the prop for the each element in the set of matched elements or set one or more attributes for every matched element. * @param [string|object] name - Name of the property to be retrieved/set. Can be object of attributes/values. * @param [string] value - Value of the property to be set. * @returns {string|Element} */ prop(name, value) { if (value !== undefined) { let nm = name; name = {}; name[nm] = value; } if (typeof name === 'object') { return this.each(function (node) { for (let key in name) { node[key] = name[key]; } }); } return this.length ? this.first()[name] : ''; } /** * Get the value of an daata attribute for the each element in the set of matched elements or set one or more attributes for every matched element. * @param [string|object] name - Name of the data attribute to be retrieved/set. Can be object of attributes/values. * @param [string] value - Value of the data attribute to be set. * @returns {object|Element} */ data(name, value) { if (!name) { return this.first().dataset; } return this.attr(name, value, true); } } export default function $(selector, element) { return new Element(selector, element); };