UNPKG

blissfuljs

Version:

Lightweight helper library for modern browsers.

877 lines (720 loc) 21.2 kB
(function() { "use strict"; function overload(callback, start, end) { start = start === undefined ? 1 : start; end = end || start + 1; if (end - start <= 1) { return function() { if (arguments.length <= start || $.type(arguments[start]) === "string") { return callback.apply(this, arguments); } var obj = arguments[start]; var ret; for (var key in obj) { var args = Array.prototype.slice.call(arguments); args.splice(start, 1, key, obj[key]); ret = callback.apply(this, args); } return ret; }; } return overload(overload(callback, start + 1, end), start, end - 1); } // Copy properties from one object to another. Overwrites allowed. // Subtle difference of array vs string whitelist: If property doesn't exist in from, array will not define it. function extend(to, from, whitelist) { var whitelistType = type(whitelist); if (whitelistType === "string") { // To copy gettters/setters, preserve flags etc var descriptor = Object.getOwnPropertyDescriptor(from, whitelist); if (descriptor && (!descriptor.writable || !descriptor.configurable || !descriptor.enumerable || descriptor.get || descriptor.set)) { delete to[whitelist]; Object.defineProperty(to, whitelist, descriptor); } else { to[whitelist] = from[whitelist]; } } else if (whitelistType === "array") { whitelist.forEach(function(property) { if (property in from) { extend(to, from, property); } }); } else { for (var property in from) { if (whitelist) { if (whitelistType === "regexp" && !whitelist.test(property) || whitelistType === "function" && !whitelist.call(from, property)) { continue; } } extend(to, from, property); } } return to; } /** * Returns the [[Class]] of an object in lowercase (eg. array, date, regexp, string etc) */ function type(obj) { if (obj === null) { return "null"; } if (obj === undefined) { return "undefined"; } var ret = (Object.prototype.toString.call(obj).match(/^\[object\s+(.*?)\]$/)[1] || "").toLowerCase(); if (ret == "number" && isNaN(obj)) { return "nan"; } return ret; } var $ = self.Bliss = extend(function(expr, context) { if (arguments.length == 2 && !context || !expr) { return null; } return $.type(expr) === "string"? (context || document).querySelector(expr) : expr || null; }, self.Bliss); extend($, { extend: extend, overload: overload, type: type, property: $.property || "_", listeners: self.WeakMap? new WeakMap() : new Map(), original: { addEventListener: (self.EventTarget || Node).prototype.addEventListener, removeEventListener: (self.EventTarget || Node).prototype.removeEventListener }, sources: {}, noop: function() {}, $: function(expr, context) { if (expr instanceof Node || expr instanceof Window) { return [expr]; } if (arguments.length == 2 && !context) { return []; } return Array.prototype.slice.call(typeof expr == "string"? (context || document).querySelectorAll(expr) : expr || []); }, /* * Return first non-undefined value. Mainly used internally. */ defined: function () { for (var i=0; i<arguments.length; i++) { if (arguments[i] !== undefined) { return arguments[i]; } } }, create: function (tag, o) { if (tag instanceof Node) { return $.set(tag, o); } // 4 signatures: (tag, o), (tag), (o), () if (arguments.length === 1) { if ($.type(tag) === "string") { o = {}; } else { o = tag; tag = o.tag; o = $.extend({}, o, function(property) { return property !== "tag"; }); } } return $.set(document.createElement(tag || "div"), o); }, each: function(obj, callback, ret) { ret = ret || {}; for (var property in obj) { ret[property] = callback.call(obj, property, obj[property]); } return ret; }, ready: function(context, callback, isVoid) { if (typeof context === "function" && !callback) { callback = context; context = undefined; } context = context || document; if (callback) { if (context.readyState !== "loading") { callback(); } else { $.once(context, "DOMContentLoaded", function() { callback(); }); } } if (!isVoid) { return new Promise(function(resolve) { $.ready(context, resolve, true); }); } }, // Helper for defining OOP-like “classes” Class: function(o) { var special = ["constructor", "extends", "abstract", "static"].concat(Object.keys($.classProps)); var init = o.hasOwnProperty("constructor")? o.constructor : $.noop; var Class; if (arguments.length == 2) { // Existing class provided Class = arguments[0]; o = arguments[1]; } else { Class = function() { if (this.constructor.__abstract && this.constructor === Class) { throw new Error("Abstract classes cannot be directly instantiated."); } Class.super && Class.super.apply(this, arguments); init.apply(this, arguments); }; Class.super = o.extends || null; Class.prototype = $.extend(Object.create(Class.super? Class.super.prototype : Object), { constructor: Class }); // For easier calling of super methods // This doesn't save us from having to use .call(this) though Class.prototype.super = Class.super? Class.super.prototype : null; Class.__abstract = !!o.abstract; } var specialFilter = function(property) { return this.hasOwnProperty(property) && special.indexOf(property) === -1; }; // Static methods if (o.static) { $.extend(Class, o.static, specialFilter); for (var property in $.classProps) { if (property in o.static) { $.classProps[property](Class, o.static[property]); } } } // Instance methods $.extend(Class.prototype, o, specialFilter); for (var property in $.classProps) { if (property in o) { $.classProps[property](Class.prototype, o[property]); } } return Class; }, // Properties with special handling in classes classProps: { // Lazily evaluated properties lazy: overload(function(obj, property, getter) { Object.defineProperty(obj, property, { get: function() { var value = getter.call(this); Object.defineProperty(this, property, { value: value, configurable: true, enumerable: true, writable: true }); return value; }, set: function(value) { // Blind write: skip running the getter Object.defineProperty(this, property, { value: value, configurable: true, enumerable: true, writable: true }); }, configurable: true, enumerable: true }); return obj; }), // Properties that behave like normal properties but also execute code upon getting/setting live: overload(function(obj, property, descriptor) { if ($.type(descriptor) === "function") { descriptor = {set: descriptor}; } Object.defineProperty(obj, property, { get: function() { var value = this["_" + property]; var ret = descriptor.get && descriptor.get.call(this, value); return ret !== undefined? ret : value; }, set: function(v) { var value = this["_" + property]; var ret = descriptor.set && descriptor.set.call(this, v, value); this["_" + property] = ret !== undefined? ret : v; }, configurable: descriptor.configurable, enumerable: descriptor.enumerable }); return obj; }) }, // Includes a script, returns a promise include: function() { var url = arguments[arguments.length - 1]; var loaded = arguments.length === 2? arguments[0] : false; var script = document.createElement("script"); return loaded? Promise.resolve() : new Promise(function(resolve, reject) { $.set(script, { async: true, onload: function() { resolve(script); script.parentNode && script.parentNode.removeChild(script); }, onerror: function() { reject(script); }, src: url, inside: document.head }); }); }, // Dynamically load a CSS or JS resource load: function load(url, base) { base = base? new URL(base, location.href) : location.href; url = new URL(url, base); // Prevent double loading var loading = load.loading = load.loading || {}; if (loading[url + ""]) { return loading[url + ""]; } if (/\.css$/.test(url.pathname)) { // CSS file return loading[url + ""] = new Promise(function(resolve, reject) { var link = $.create("link", { "href": url, "rel": "stylesheet", "inside": document.head, onload: function() { resolve(link); }, onerror: function() { reject(link); } }); }); } // JS file return loading[url + ""] = $.include(url); }, /* * Fetch API inspired XHR wrapper. Returns promise. */ fetch: function(url, o) { if (!url) { throw new TypeError("URL parameter is mandatory and cannot be " + url); } // Set defaults & fixup arguments var env = extend({ url: new URL(url, location), data: "", method: "GET", headers: {}, xhr: new XMLHttpRequest() }, o); env.method = env.method.toUpperCase(); $.hooks.run("fetch-args", env); // Start sending the request if (env.method === "GET" && env.data) { env.url.search += env.data; } document.body.setAttribute("data-loading", env.url); env.xhr.open(env.method, env.url.href, env.async !== false, env.user, env.password); for (var property in o) { if (property === "upload") { if (env.xhr.upload && typeof o[property] === "object") { $.extend(env.xhr.upload, o[property]); } } else if (property in env.xhr) { try { env.xhr[property] = o[property]; } catch (e) { self.console && console.error(e); } } } var headerKeys = Object.keys(env.headers).map(function(key) { return key.toLowerCase(); }); if (env.method !== "GET" && headerKeys.indexOf("content-type") === -1) { env.xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); } for (var header in env.headers) { if (env.headers[header] !== undefined) { env.xhr.setRequestHeader(header, env.headers[header]); } } var promise = new Promise(function(resolve, reject) { env.xhr.onload = function() { document.body.removeAttribute("data-loading"); if (env.xhr.status === 0 || env.xhr.status >= 200 && env.xhr.status < 300 || env.xhr.status === 304) { // Success! resolve(env.xhr); } else { reject($.extend(Error(env.xhr.statusText), { xhr: env.xhr, get status() { return this.xhr.status; } })); } }; env.xhr.onerror = function() { document.body.removeAttribute("data-loading"); reject($.extend(Error("Network Error"), {xhr: env.xhr})); }; env.xhr.ontimeout = function() { document.body.removeAttribute("data-loading"); reject($.extend(Error("Network Timeout"), {xhr: env.xhr})); }; env.xhr.send(env.method === "GET"? null : env.data); }); // Hack: Expose xhr.abort(), by attaching xhr to the promise. promise.xhr = env.xhr; return promise; }, value: function(obj) { var hasRoot = typeof obj !== "string"; return $.$(arguments).slice(+hasRoot).reduce(function(obj, property) { return obj && obj[property]; }, hasRoot? obj : self); } }); $.Hooks = new $.Class({ add: function (name, callback, first) { if (typeof arguments[0] != "string") { // Multiple hooks for (var name in arguments[0]) { this.add(name, arguments[0][name], arguments[1]); } return; } (Array.isArray(name)? name : [name]).forEach(function(name) { this[name] = this[name] || []; if (callback) { this[name][first? "unshift" : "push"](callback); } }, this); }, run: function (name, env) { this[name] = this[name] || []; this[name].forEach(function(callback) { callback.call(env && env.context? env.context : env, env); }); } }); $.hooks = new $.Hooks(); var _ = $.property; $.Element = function (subject) { this.subject = subject; // Author-defined element-related data this.data = {}; // Internal Bliss element-related data this.bliss = {}; }; $.Element.prototype = { set: overload(function(property, value) { if (property in $.setProps) { $.setProps[property].call(this, value); } else if (property in this) { this[property] = value; } else { this.setAttribute(property, value); } }, 0), // Run a CSS transition, return promise transition: function(props, duration) { return new Promise(function(resolve, reject) { if ("transition" in this.style && duration !== 0) { // Get existing style var previous = $.extend({}, this.style, /^transition(Duration|Property)$/); $.style(this, { transitionDuration: (duration || 400) + "ms", transitionProperty: Object.keys(props).join(", ") }); $.once(this, "transitionend", function() { clearTimeout(i); $.style(this, previous); resolve(this); }); // Failsafe, in case transitionend doesn’t fire var i = setTimeout(resolve, duration+50, this); $.style(this, props); } else { $.style(this, props); resolve(this); } }.bind(this)); }, // Fire a synthesized event on the element fire: function (type, properties) { var evt = document.createEvent("HTMLEvents"); evt.initEvent(type, true, true ); // Return the result of dispatching the event, so we // can know if `e.preventDefault` was called inside it return this.dispatchEvent($.extend(evt, properties)); }, bind: overload(function(types, options) { if (arguments.length > 1 && ($.type(options) === "function" || options.handleEvent)) { // options is actually callback var callback = options; options = $.type(arguments[2]) === "object"? arguments[2] : { capture: !!arguments[2] // in case it's passed as a boolean 3rd arg }; options.callback = callback; } var listeners = $.listeners.get(this) || {}; types.trim().split(/\s+/).forEach(function (type) { if (type.indexOf(".") > -1) { type = type.split("."); var className = type[1]; type = type[0]; } listeners[type] = listeners[type] || []; if (listeners[type].filter(function(l) { return l.callback === options.callback && l.capture == options.capture; }).length === 0) { listeners[type].push($.extend({className: className}, options)); } $.original.addEventListener.call(this, type, options.callback, options); }, this); $.listeners.set(this, listeners); }, 0), unbind: overload(function(types, options) { if (options && ($.type(options) === "function" || options.handleEvent)) { var callback = options; options = arguments[2]; } if ($.type(options) == "boolean") { options = {capture: options}; } options = options || {}; options.callback = options.callback || callback; var listeners = $.listeners.get(this); (types || "").trim().split(/\s+/).forEach(function (type) { if (type.indexOf(".") > -1) { type = type.split("."); var className = type[1]; type = type[0]; } //if listeners exist, always go through listeners to clean up if (!listeners) { if (type && options.callback) { return $.original.removeEventListener.call(this, type, options.callback, options.capture); } return; } // Mass unbinding, need to go through listeners for (var ltype in listeners) { if (!type || ltype === type) { // No forEach, because we’re mutating the array for (var i=0, l; l=listeners[ltype][i]; i++) { if ((!className || className === l.className) && (!options.callback || options.callback === l.callback) && (!!options.capture == !!l.capture || !type && !options.callback && undefined === options.capture) ) { listeners[ltype].splice(i, 1); $.original.removeEventListener.call(this, ltype, l.callback, l.capture); i--; } } } } }, this); }, 0), // Return a promise that resolves when an event fires, then unbind when: function(type, test) { var me = this; return new Promise(function(resolve) { me.addEventListener(type, function callee(evt) { if (!test || test.call(this, evt)) { this.removeEventListener(type, callee); resolve(evt); } }); }); }, toggleAttribute: function(name, value, test) { if (arguments.length < 3) { test = value !== null; } if (test) { this.setAttribute(name, value); } else { this.removeAttribute(name); } } }; /* * Properties with custom handling in $.set() * Also available as functions directly on element._ and on $ */ $.setProps = { // Set a bunch of inline CSS styles style: function (val) { for (var property in val) { if (property in this.style) { // camelCase versions this.style[property] = val[property]; } else { // This way we can set CSS Variables too and use normal property names this.style.setProperty(property, val[property]); } } }, // Set a bunch of attributes attributes: function (o) { for (var attribute in o) { this.setAttribute(attribute, o[attribute]); } }, // Set a bunch of properties on the element properties: function (val) { $.extend(this, val); }, // Bind one or more events to the element events: function (val) { if (arguments.length == 1 && val && val.addEventListener) { // Copy events from other element (requires Bliss Full) var me = this; // Copy listeners if ($.listeners) { var listeners = $.listeners.get(val); for (var type in listeners) { listeners[type].forEach(function(l) { $.bind(me, type, l.callback, l.capture); }); } } // Copy inline events for (var onevent in val) { if (onevent.indexOf("on") === 0) { this[onevent] = val[onevent]; } } } else { return $.bind.apply(this, [this].concat($.$(arguments))); } }, once: overload(function(types, callback) { var me = this; var once = function() { $.unbind(me, types, once); return callback.apply(me, arguments); }; $.bind(this, types, once, {once: true}); }, 0), // Event delegation delegate: overload(function (type, selector, callback) { $.bind(this, type, function(evt) { if (evt.target.closest(selector)) { callback.call(this, evt); } }); }, 0, 2), // Set the contents as a string, an element, an object to create an element or an array of these contents: function (val) { if (val || val === 0) { (Array.isArray(val)? val : [val]).forEach(function (child) { var type = $.type(child); if (/^(string|number)$/.test(type)) { child = document.createTextNode(child + ""); } else if (type === "object") { child = $.create(child); } if (child instanceof Node) { this.appendChild(child); } }, this); } }, // Append the element inside another element inside: function (element) { element && element.appendChild(this); }, // Insert the element before another element before: function (element) { element && element.parentNode.insertBefore(this, element); }, // Insert the element after another element after: function (element) { element && element.parentNode.insertBefore(this, element.nextSibling); }, // Insert the element before another element's contents start: function (element) { element && element.insertBefore(this, element.firstChild); }, // Wrap the element around another element around: function (element) { if (element && element.parentNode) { $.before(this, element); } this.appendChild(element); } }; $.Array = function (subject) { this.subject = subject; }; $.Array.prototype = { all: function(method) { var args = $.$(arguments).slice(1); return this[method].apply(this, args); } }; // Extends Bliss with more methods $.add = overload(function(method, callback, on, noOverwrite) { on = $.extend({$: true, element: true, array: true}, on); if ($.type(callback) == "function") { if (on.element && (!(method in $.Element.prototype) || !noOverwrite)) { $.Element.prototype[method] = function () { return this.subject && $.defined(callback.apply(this.subject, arguments), this.subject); }; } if (on.array && (!(method in $.Array.prototype) || !noOverwrite)) { $.Array.prototype[method] = function() { var args = arguments; return this.subject.map(function(element) { return element && $.defined(callback.apply(element, args), element); }); }; } if (on.$) { $.sources[method] = $[method] = callback; if (on.array || on.element) { $[method] = function () { var args = [].slice.apply(arguments); var subject = args.shift(); var Type = on.array && Array.isArray(subject)? "Array" : "Element"; return $[Type].prototype[method].apply({subject: subject}, args); }; } } } }, 0); $.add($.Array.prototype, {element: false}); $.add($.Element.prototype); $.add($.setProps); $.add($.classProps, {element: false, array: false}); // Add native methods on $ and _ var dummy = document.createElement("_"); $.add($.extend({}, HTMLElement.prototype, function(method) { return $.type(dummy[method]) === "function"; }), null, true); })();