assets/js/romo/base.js in romo-0.19.10 vs assets/js/romo/base.js in romo-0.20.0

- old
+ new

@@ -1,165 +1,255 @@ var Romo = function() { - this._eventCallbacks = []; + this.popupStack = new RomoPopupStack(); + this.parentChildElems = new RomoParentChildElems(); } -// TODO: rework w/o jQuery Romo.prototype.doInit = function() { - this.parentChildElems = new RomoParentChildElems(); - - $.each(this._eventCallbacks, function(idx, eventCallback) { - $('body').on(eventCallback.eventName, eventCallback.callback); - }); - - this.triggerInitUI($('body')); + this.popupStack.doInit(); + this.parentChildElems.doInit(); + this.initElems(Romo.f('body')); } // element finders Romo.prototype.f = function(selector) { return this.array(document.querySelectorAll(selector)); } -Romo.prototype.find = function(parentElem, selector) { - return this.array(parentElem.querySelectorAll(selector)); +Romo.prototype.find = function(parentElems, selector) { + return this.array(parentElems).reduce( + Romo.proxy(function(foundElems, parentElem) { + return foundElems.concat(this.array(parentElem.querySelectorAll(selector))); + }, this), + [] + ); } -Romo.prototype.children = function(parentElem) { - return this.array(parentElem.children); +Romo.prototype.is = function(elem, selector) { + return ( + elem.matches || + elem.matchesSelector || + elem.msMatchesSelector || + elem.mozMatchesSelector || + elem.webkitMatchesSelector || + elem.oMatchesSelector + ).call(elem, selector); +}; + +Romo.prototype.children = function(parentElem, selector) { + var childElems = this.array(parentElem.children); + if (selector) { + return childElems.filter(function(childElem) { + return Romo.is(childElem, selector); + }); + } else { + return childElems; + } } Romo.prototype.parent = function(childElem) { return childElem.parentNode; } -Romo.prototype.siblings = function(elem) { - return Array.prototype.filter.call(elem.parentNode.children, function(childElem) { - return childElem !== elem; - }); +Romo.prototype.parents = function(childElem, selector) { + var parentElem = this.parent(childElem); + if (parentElem && parentElem !== document) { + if (!selector || Romo.is(parentElem, selector)) { + if (Romo.is(parentElem, 'body')) { + return [parentElem]; + } else { + return [parentElem].concat(this.parents(parentElem, selector)); + } + } else { + if (Romo.is(parentElem, 'body')) { + return []; + } else { + return this.parents(parentElem, selector); + } + } + } else { + return []; + } } -Romo.prototype.prev = function(fromElem) { - return fromElem.previousElementSibling; +Romo.prototype.scrollableParents = function(childElem, selector) { + return Romo.parents(childElem, selector).filter(function(parentElem) { + return ( + Romo._overflowScrollableRegex.test(Romo.css(parentElem, 'overflow')) || + Romo._overflowScrollableRegex.test(Romo.css(parentElem, 'overflow-y')) || + Romo._overflowScrollableRegex.test(Romo.css(parentElem, 'overflow-x')) + ); + }); } -Romo.prototype.next = function(fromElem) { - return fromElem.nextElementSibling; -} - Romo.prototype.closest = function(fromElem, selector) { if (fromElem.closest) { return fromElem.closest(selector); } else { - var matchesSelector = fromElem.matches || - fromElem.webkitMatchesSelector || - fromElem.mozMatchesSelector || - fromElem.msMatchesSelector; while (fromElem) { - if (matchesSelector.call(fromElem, selector)) { + if (Romo.is(fromElem, selector)) { return fromElem; } else { fromElem = fromElem.parentElement; } } return undefined; } } -// TODO: rework w/o jQuery -Romo.prototype.selectNext = function(elem, selector) { - // like `$().next()`, but takes a selector; returns first next elem that - // matches the given selector or an empty collection if non matches - var el = elem.next(); - while(el.length) { - if (selector === undefined || el.is(selector)) { - return el; +Romo.prototype.siblings = function(elem, selector) { + return Romo.children(Romo.parent(elem), selector).filter(function(childElem) { + return childElem !== elem; + }); +} + +Romo.prototype.prev = function(fromElem, selector) { + var elem = fromElem.previousElementSibling; + if (elem === null) { + elem = undefined; + } + + while(elem) { + if (!selector || Romo.is(elem, selector)) { + return elem; } - el = el.next(); + elem = elem.previousElementSibling; + if (elem === null) { + elem = undefined; + } } - return el; + return elem; } -// TODO: rework w/o jQuery -Romo.prototype.selectPrev = function(elem, selector) { - // like `$().prev()`, but takes a selector; returns first prev elem that - // matches the given selector or an empty collection if non matches - var el = elem.prev(); - while(el.length) { - if (selector === undefined || el.is(selector)) { - return el; +Romo.prototype.next = function(fromElem, selector) { + var elem = fromElem.nextElementSibling; + if (elem === null) { + elem = undefined; + } + + while(elem) { + if (!selector || Romo.is(elem, selector)) { + return elem; } - el = el.prev(); + elem = elem.nextElementSibling; + if (elem === null) { + elem = undefined; + } } - return el; + return elem; } // attributes, styles, classes Romo.prototype.attr = function(elem, attrName) { - return elem.getAttribute ? elem.getAttribute(attrName) : undefined; + var a = elem.getAttribute(attrName); + if (a === null) { + return undefined; + } else { + return a; + } } -Romo.prototype.setAttr = function(elem, attrName, attrValue) { - if (elem.setAttribute) { - elem.setAttribute(attrName, attrValue); - } - return attrValue; +Romo.prototype.setAttr = function(elems, attrName, attrValue) { + var v = String(attrValue); + Romo.array(elems).forEach(function(elem) { + elem.setAttribute(attrName, v); + }); + return v; } +Romo.prototype.rmAttr = function(elems, attrName) { + Romo.array(elems).forEach(function(elem) { + elem.removeAttribute(attrName); + }); +} + Romo.prototype.data = function(elem, dataName) { return this._deserializeValue(this.attr(elem, "data-"+dataName)); } -Romo.prototype.setData = function(elem, dataName, dataValue) { - return this.setAttr(elem, "data-"+dataName, String(dataValue)); +Romo.prototype.setData = function(elems, dataName, dataValue) { + return this.setAttr(elems, "data-"+dataName, dataValue); } +Romo.prototype.rmData = function(elems, dataName) { + this.rmAttr(elems, "data-"+dataName); +} + Romo.prototype.style = function(elem, styleName) { return elem.style[styleName]; } -Romo.prototype.setStyle = function(elem, styleName, styleValue) { - elem.style[styleName] = styleValue; +Romo.prototype.setStyle = function(elems, styleName, styleValue) { + Romo.array(elems).forEach(function(elem) { + elem.style[styleName] = styleValue; + }); return styleValue; } +Romo.prototype.rmStyle = function(elems, styleName) { + Romo.array(elems).forEach(function(elem) { + elem.style[styleName] = ''; + }); +} + Romo.prototype.css = function(elem, styleName) { return window.getComputedStyle(elem, null).getPropertyValue(styleName); } -Romo.prototype.addClass = function(elem, className) { - elem.classList.add(className); +Romo.prototype.addClass = function(elems, className) { + var classNames = className.split(' ').filter(function(n){ return n; }); + Romo.array(elems).forEach(function(elem) { + classNames.forEach(function(name) { + elem.classList.add(name); + }); + }); return className; } -Romo.prototype.removeClass = function(elem, className) { - elem.classList.remove(className); +Romo.prototype.removeClass = function(elems, className) { + var classNames = className.split(' ').filter(function(n){ return n; }); + Romo.array(elems).forEach(function(elem) { + classNames.forEach(function(name) { + elem.classList.remove(name); + }); + }); return className; } -Romo.prototype.toggleClass = function(elem, className) { - elem.classList.toggle(className); +Romo.prototype.toggleClass = function(elems, className) { + var classNames = className.split(' ').filter(function(n){ return n; }); + Romo.array(elems).forEach(function(elem) { + classNames.forEach(function(name) { + elem.classList.toggle(name); + }); + }); return className; } Romo.prototype.hasClass = function(elem, className) { return elem.classList.contains(className); } -Romo.prototype.show = function(elem) { - elem.style.display = ''; +Romo.prototype.show = function(elems) { + Romo.array(elems).forEach(function(elem) { + elem.style.display = ''; + }); } -Romo.prototype.hide = function(elem) { - elem.style.display = 'none'; +Romo.prototype.hide = function(elems) { + Romo.array(elems).forEach(function(elem) { + elem.style.display = 'none'; + }); } Romo.prototype.offset = function(elem) { - var rect = elem.getBoundingClientRect(); + var elemRect = elem.getBoundingClientRect(); + var bodyRect = document.body.getBoundingClientRect(); return { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft + top: elemRect.top - bodyRect.top, + left: elemRect.left - bodyRect.left }; } Romo.prototype.scrollTop = function(elem) { return ('scrollTop' in elem) ? elem.scrollTop : elem.pageYOffset; @@ -167,236 +257,359 @@ Romo.prototype.scrollLeft = function(elem) { return ('scrollLeft' in elem) ? elem.scrollLeft : elem.pageXOffset; } -Romo.prototype.setScrollTop = function(elem, value) { - if ('scrollTop' in elem) { - elem.scrollTop = value; - } else { - elem.scrollTo(elem.scrollX, value); - } +Romo.prototype.setScrollTop = function(elems, value) { + Romo.array(elems).forEach(function(elem) { + if ('scrollTop' in elem) { + elem.scrollTop = value; + } else { + elem.scrollTo(elem.scrollX, value); + } + }); } -Romo.prototype.setScrollLeft = function(elem, value) { - if ('scrollLeft' in elem) { - elem.scrollLeft = value; - } else { - elem.scrollTo(value, elem.scrollY); - } +Romo.prototype.setScrollLeft = function(elems, value) { + Romo.array(elems).forEach(function(elem) { + if ('scrollLeft' in elem) { + elem.scrollLeft = value; + } else { + elem.scrollTo(value, elem.scrollY); + } + }); } -// TODO: rework w/o jQuery -Romo.prototype.getComputedStyle = function(node, styleName) { - return window.getComputedStyle(node, null).getPropertyValue(styleName); -} - -// TODO: rework w/o jQuery Romo.prototype.parseZIndex = function(elem) { // for the case where z-index is set directly on the elem var val = this.parseElemZIndex(elem); if (val !== 0) { return val; } // for the case where z-index is inherited from a parent elem - var parentIndexes = this.toArray(elem.parents()).reduce($.proxy(function(prev, curr) { - var pval = this.parseElemZIndex($(curr)); - if (pval !== 0) { - prev.push(pval); + var pval = 0; + var parentIndexes = Romo.parents(elem).forEach(Romo.proxy(function(parentElem) { + if (pval === 0) { + pval = this.parseElemZIndex(parentElem); } - return prev; - }, this), []); - parentIndexes.push(0); // in case z-index is 'auto' all the way up - return parentIndexes[0]; + }, this)); + + // z-index is 'auto' all the way up + return pval; } -// TODO: rework w/o jQuery Romo.prototype.parseElemZIndex = function(elem) { - var val = parseInt(this.getComputedStyle(elem[0], "z-index")); + var val = parseInt(this.css(elem, "z-index"), 10); if (!isNaN(val)) { return val; } return 0; } -// DOM manipulation +// elems init Romo.prototype.elems = function(htmlString) { var context = document.implementation.createHTMLDocument(); // Set the base href for the created document so any parsed // elements with URLs are based on the document's URL var base = context.createElement('base'); base.href = document.location.href; context.head.appendChild(base); - context.body.innerHTML = htmlString; - return this.array(context.body.children); + var results = Romo._elemsTagNameRegEx.exec(htmlString); + if (!results){ return []; } + + var tagName = results[1].toLowerCase(); + var wrap = Romo._elemsWrapMap[tagName]; + if (!wrap) { + context.body.innerHTML = htmlString; + return this.array(context.body.children); + } else { + context.body.innerHTML = wrap[1] + htmlString + wrap[2]; + var parentElem = context.body; + var i = wrap[0]; + while(i-- !== 0) { + parentElem = parentElem.lastChild; + } + return this.array(parentElem.children) + } } -Romo.prototype.remove = function(elem) { - return elem.parentNode.removeChild(elem); +Romo.prototype.addElemsInitSelector = function(selector, componentClass) { + this._elemsInitComponents[selector] = componentClass; } +Romo.prototype.initElems = function(elems) { + return this._elemsInitTrigger(Romo.array(elems)); +} + +Romo.prototype.initElemsHtml = function(htmlString) { + return this.initElems(this.elems(htmlString)); +} + +// DOM manipulation + +Romo.prototype.remove = function(elems) { + return Romo.array(elems).map(function(elem) { + if (elem.parentNode) { + elem.parentNode.removeChild(elem); + } + return elem; + }); +} + Romo.prototype.replace = function(elem, replacementElem) { elem.parentNode.replaceChild(replacementElem, elem); return replacementElem; } Romo.prototype.replaceHtml = function(elem, htmlString) { - return this.replace(elem, this.elems(htmlString)[0]); + var replacementElem = Romo.elems(htmlString)[0]; + if (replacementElem === undefined && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain an HTML element."); + } + return this.replace(elem, replacementElem); } +Romo.prototype.initReplace = function(elem, replacementElem) { + return this.initElems(this.replace(elem, replacementElem))[0]; +} + +Romo.prototype.initReplaceHtml = function(elem, htmlString) { + return this.initElems(this.replaceHtml(elem, htmlString))[0]; +} + Romo.prototype.update = function(elem, childElems) { elem.innerHTML = ''; - return childElems.map(function(childElem) { + return Romo.array(childElems).map(function(childElem) { return elem.appendChild(childElem); }); } Romo.prototype.updateHtml = function(elem, htmlString) { - return this.update(elem, this.elems(htmlString)); + var childElems = Romo.elems(htmlString); + if (childElems.length === 0 && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain any HTML elements."); + } + return this.update(elem, childElems); } +Romo.prototype.updateText = function(elem, textString) { + elem.innerText = textString; +} + +Romo.prototype.initUpdate = function(elem, childElems) { + return this.initElems(this.update(elem, childElems)); +} + +Romo.prototype.initUpdateHtml = function(elem, htmlString) { + return this.initElems(this.updateHtml(elem, htmlString)); +} + Romo.prototype.prepend = function(elem, childElems) { var refElem = elem.firstChild; - return childElems.reverse().map(function(childElem) { + return Romo.array(childElems).reverse().map(function(childElem) { refElem = elem.insertBefore(childElem, refElem); return refElem; }).reverse(); } Romo.prototype.prependHtml = function(elem, htmlString) { - return this.prepend(elem, this.elems(htmlString)); + var childElems = Romo.elems(htmlString); + if (childElems.length === 0 && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain any HTML elements."); + } + return this.prepend(elem, childElems); } +Romo.prototype.initPrepend = function(elem, childElems) { + return this.initElems(this.prepend(elem, childElems)); +} + +Romo.prototype.initPrependHtml = function(elem, htmlString) { + return this.initElems(this.prependHtml(elem, htmlString)); +} + Romo.prototype.append = function(elem, childElems) { - return childElems.map(function(childElem) { + return Romo.array(childElems).map(function(childElem) { return elem.appendChild(childElem); }); } Romo.prototype.appendHtml = function(elem, htmlString) { - return this.append(elem, this.elems(htmlString)); + var childElems = Romo.elems(htmlString); + if (childElems.length === 0 && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain any HTML elements."); + } + return this.append(elem, childElems); } +Romo.prototype.initAppend = function(elem, childElems) { + return this.initElems(this.append(elem, childElems)); +} + +Romo.prototype.initAppendHtml = function(elem, htmlString) { + return this.initElems(this.appendHtml(elem, htmlString)); +} + Romo.prototype.before = function(elem, siblingElems) { var refElem = elem; var parentElem = elem.parentNode; - return siblingElems.reverse().map(function(siblingElem) { + return Romo.array(siblingElems).reverse().map(function(siblingElem) { refElem = parentElem.insertBefore(siblingElem, refElem); return refElem; }).reverse(); } Romo.prototype.beforeHtml = function(elem, htmlString) { - return this.before(elem, this.elems(htmlString)); + var siblingElems = Romo.elems(htmlString); + if (siblingElems.length === 0 && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain any HTML elements."); + } + return this.before(elem, siblingElems); } +Romo.prototype.initBefore = function(elem, siblingElems) { + return this.initElems(this.before(elem, siblingElems)); +} + +Romo.prototype.initBeforeHtml = function(elem, htmlString) { + return this.initElems(this.beforeHtml(elem, htmlString)); +} + Romo.prototype.after = function(elem, siblingElems) { var refElem = this.next(elem); var parentElem = elem.parentNode; - return siblingElems.map(function(siblingElem) { + return Romo.array(siblingElems).map(function(siblingElem) { return parentElem.insertBefore(siblingElem, refElem); }); } Romo.prototype.afterHtml = function(elem, htmlString) { - return this.after(elem, this.elems(htmlString)); + var siblingElems = Romo.elems(htmlString); + if (siblingElems.length === 0 && (typeof(htmlString) !== 'string' || htmlString.trim() !== '')) { + throw new Error("Invalid HTML string, doesn't contain any HTML elements."); + } + return this.after(elem, siblingElems); } -// init UI (TODO: rework w/o jQuery) - -Romo.prototype.onInitUI = function(callback) { - this._addEventCallback('romo:initUI', callback); +Romo.prototype.initAfter = function(elem, siblingElems) { + return this.initElems(this.after(elem, siblingElems)); } -Romo.prototype.triggerInitUI = function(elems) { - elems.trigger('romo:initUI'); +Romo.prototype.initAfterHtml = function(elem, htmlString) { + return this.initElems(this.afterHtml(elem, htmlString)); } -Romo.prototype.initUIElems = function(e, selector) { - var elems = $(e.target).find(selector).get(); - if ($(e.target).is(selector)) { - elems.push(e.target) +// events + +Romo.prototype.on = function(elems, eventName, fn) { + var proxyFn = function(e) { + var result = fn.apply(e.target, e.detail === undefined ? [e] : [e].concat(e.detail)); + if (result === false) { + e.preventDefault(); + e.stopPropagation(); + } + return result; } - return $(elems); -} + proxyFn._romofid = this._fn(fn)._romofid; -Romo.prototype.initHtml = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var htmlElems = $(data) - $(elem).html(htmlElems); - this.triggerInitUI(htmlElems); + Romo.array(elems).forEach(Romo.proxy(function(elem) { + elem.addEventListener(eventName, proxyFn); + var key = this._handlerKey(elem, eventName, proxyFn); + if (this._handlers[key] === undefined) { + this._handlers[key] = []; + } + this._handlers[key].push(proxyFn); }, this)); } -Romo.prototype.initReplace = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var replacementElem = $(data); - $(elem).replaceWith(replacementElem); - this.triggerInitUI(replacementElem); +Romo.prototype.off = function(elems, eventName, fn) { + Romo.array(elems).forEach(Romo.proxy(function(elem) { + var key = this._handlerKey(elem, eventName, fn); + (this._handlers[key] || []).forEach(function(proxyFn) { + elem.removeEventListener(eventName, proxyFn); + }); + this._handlers[key] = []; }, this)); } -Romo.prototype.initPrepend = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var prependedElem = $(data); - $(elem).prepend(prependedElem); - this.triggerInitUI(prependedElem); - }, this)); +Romo.prototype.trigger = function(elems, customEventName, args) { + var event = undefined; + if (typeof window.CustomEvent === "function") { + event = new CustomEvent(customEventName, { detail: args }); + } else { + event = document.createEvent('CustomEvent'); + event.initCustomEvent(customEventName, false, false, args); + } + Romo.array(elems).forEach(function(elem) { + elem.dispatchEvent(event); + }); } -Romo.prototype.initAppend = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var appendedElem = $(data); - $(elem).append(appendedElem); - this.triggerInitUI(appendedElem); - }, this)); +Romo.prototype.pushFn = function(fn) { + // push the function to delay running until the end of the reactor stack + setTimeout(fn, 1); } -Romo.prototype.initBefore = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var insertedElem = $(data); - $(elem).before(insertedElem); - this.triggerInitUI(insertedElem); - }, this)); +Romo.prototype.ready = function(eventHandlerFn) { + if (document.readyState === 'complete' || document.readyState !== 'loading') { + eventHandlerFn(); + } else { + this.on(document, 'DOMContentLoaded', eventHandlerFn); + } } -Romo.prototype.initAfter = function(elems, data) { - elems.each($.proxy(function(index, elem) { - var insertedElem = $(data); - $(elem).after(insertedElem); - this.triggerInitUI(insertedElem); - }, this)); -} - // page handling Romo.prototype.reloadPage = function() { window.location = window.location; } Romo.prototype.redirectPage = function(redirectUrl) { window.location = redirectUrl; } -// param serialization (TODO: rework w/o jQuery) +// param serialization +// Romo.param({ a: 2, b: 'three', c: 4 }); #=> "a=2&b=three&c=4" +// Romo.param({ a: [ 2, 3, 4 ] }); #=> "a[]=2&a[]=3&a[]=4" +// Romo.param({ a: 2, b: '', c: 4 }); #=> "a=2&b=&c=4" +// Romo.param({ a: 2, b: '', c: 4 }, { removeEmpty: true }); #=> "a=2&c=4" +// Romo.param({ a: [ 2, 3, 4 ], b: [''] }); #=> "a[]=2&a[]=3&a[]=4&b[]=" +// Romo.param({ a: [ 2, 3, 4 ], b: [''] }, { removeEmpty: true }); #=> "a[]=2&a[]=3&a[]=4" +// Romo.param({ a: '123-ABC' }); #=> "a=123%2DABC" +// Romo.param({ a: '123-ABC' }, { decodeValues: true }); #=> "a=123-ABC" + Romo.prototype.param = function(data, opts) { - var paramData = $.extend({}, data); + var keyValues = []; - if (opts && opts.removeEmpty) { - $.each(paramData, function(key, value) { - if (value === '') { - delete paramData[key]; + var processKeyValue = function(keyValues, key, value, opts) { + var v = String(value); + if (!opts || !opts.removeEmpty || v !== '') { + keyValues.push([key, v]); + } + } + + for (var key in data) { + var value = data[key]; + if (Array.isArray(value)) { + if (!opts || !opts.removeEmpty || value.length !== 0) { + value.forEach(function(listValue) { + processKeyValue(keyValues, key+'[]', listValue, opts); + }) } - }) + } else { + processKeyValue(keyValues, key, value, opts); + } } - var paramString = $.param(paramData); + var paramString = keyValues.map(function(keyValue){ + return keyValue.join('='); + }).join('&'); if (opts && opts.decodeValues) { paramString = this.decodeParam(paramString); } @@ -441,17 +654,29 @@ // AJAX Romo.prototype.ajax = function(settings) { var httpMethod = (settings.type || 'GET').toUpperCase(); var xhrUrl = settings.url || window.location.toString(); - var xhrData = settings.data ? settings.data : null - if (xhrData && httpMethod === 'GET') { - var xhrQuery = Romo.param(xhrData); - if (xhrQuery !== '') { - xhrUrl = (xhrUrl + '&' + xhrQuery).replace(/[&?]{1,2}/, '?'); + var xhrData = settings.data ? settings.data : undefined + if (xhrData && Object.keys(xhrData).length > 0) { + if (httpMethod === 'GET') { + var xhrQuery = Romo.param(xhrData); + if (xhrQuery !== '') { + xhrUrl = (xhrUrl + '&' + xhrQuery).replace(/[&?]{1,2}/, '?'); + } + xhrData = undefined; + } else { + var formData = new FormData; + for (var name in xhrData) { + Romo.array(xhrData[name]).forEach(function(value){ + formData.append(name, value) + }); + } + xhrData = formData; } - xhrData = null; + } else { + xhrData = undefined; } var xhr = new XMLHttpRequest(); xhr.open(httpMethod, xhrUrl, true, settings.username, settings.password); if (settings.responseType === 'arraybuffer' || settings.responseType === 'blob') { @@ -464,100 +689,111 @@ if (settings.contentType) { xhr.setRequestHeader('Content-Type', settings.contentType); } xhr.onreadystatechange = function() { if (xhr.readyState === 4) { - if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { + if (settings.success !== undefined && ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)) { if (xhr.responseType === 'arraybuffer' || xhr.responseType === 'blob') { settings.success.call(window, xhr.response, xhr.status, xhr, settings); } else { settings.success.call(window, xhr.responseText, xhr.status, xhr, settings); } - } else { + } else if(settings.error !== undefined) { settings.error.call(window, xhr.statusText || null, xhr.status, xhr, settings); } } }; xhr.send(xhrData); }, -// events +// utils -Romo.prototype.on = function(elem, eventName, fn) { - // var proxyFn = function(e) { - // var result = fn.apply(elem, e.detail === undefined ? [e] : [e].concat(e.detail)); - // if (result === false) { - // e.preventDefault(); - // e.stopPropagation(); - // } - // return result; - // } - // proxyFn._romofid = this._fn(fn)._romofid; +Romo.prototype.array = function(value) { + // short circuit `Romo.f`, `Romo.find`, and `Romo.elems` calls (and others + // that return NodeList or HTMLCollection objects), this ensures these calls + // remain fast and don't run through all of the logic to detect if an object + // is like an array + var valString = Object.prototype.toString.call(value); + if ( + valString === '[object NodeList]' || + valString === '[object HTMLCollection]' || + Array.isArray(value) + ) { + return Array.prototype.slice.call(value) + } - // var key = this._handlerKey(elem, eventName, proxyFn); - // if (!this._handlers[key]) { - // elem.addEventListener(eventName, proxyFn); - // this._handlers[key] = proxyFn; - // } + // short circuit for passing individual elems and "not truthy" values, this + // ensures these remain fast (the individual elems) and avoids running into + // the is like an array logic; this fixes issues with select and form elems + // being like an array and returning unexpected results. This also fixes + // passing in null/undefined values. + if (!value || typeof(value.nodeType) === 'number') { + return [value]; + } - // Giant Hack to temporarily support jQuery and non-jQuery triggers - // see: https://bugs.jquery.com/ticket/11047 - $(elem).on(eventName, fn); -} + var object = Object(value) + var length = undefined; + if (!!object && 'length' in object) { + length = object.length; + } -Romo.prototype.off = function(elem, eventName, fn) { - // var key = this._handlerKey(elem, eventName, fn); - // var proxyFn = this._handlers[key]; - // if (proxyFn) { - // elem.removeEventListener(eventName, proxyFn); - // this._handlers[key] = undefined; - // } - - // Giant Hack to temporarily support jQuery and non-jQuery triggers - // see: https://bugs.jquery.com/ticket/11047 - $(elem).off(eventName, fn); -} - -Romo.prototype.trigger = function(elem, customEventName, args) { - // var event = undefined; - // if (typeof window.CustomEvent === "function") { - // event = new CustomEvent(customEventName, { detail: args }); - // } else { - // event = document.createEvent('CustomEvent'); - // event.initCustomEvent(customEventName, false, false, args); - // } - // elem.dispatchEvent(event); - $(elem).trigger(customEventName, args); -} - -Romo.prototype.ready = function(eventHandlerFn) { - if (document.readyState === 'complete' || document.readyState !== 'loading') { - eventHandlerFn(); + // some browsers return 'function' for HTML elements + var isFunction = ( + typeof(object) === 'function' && + typeof(object.nodeType) !== 'number' + ); + var likeArray = ( + typeof(value) !== 'string' && + !isFunction && + object !== window && + ( Array.isArray(object) || + length === 0 || + ( typeof(length) === 'number' && + length > 0 && + (length - 1) in object + ) + ) + ); + if(likeArray) { + return Array.prototype.slice.call(value); } else { - this.on(document, 'DOMContentLoaded', eventHandlerFn); + return [value]; } } -// utils +Romo.prototype.assign = function(target) { + if(Object.assign) { + return Object.assign.apply(Object, arguments); + } else { + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + var to = Object(target); -Romo.prototype.array = function(collection) { - return Array.prototype.slice.call(collection); + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + } } Romo.prototype.proxy = function(fn, context) { var proxyFn = fn.bind(context); proxyFn._romofid = this._fn(fn)._romofid; return proxyFn; } -// TODO: rework w/o jQuery -Romo.prototype.toArray = function(elems) { - // converts a collection of elements `$()` to an array of nodes - return $.map(elems, function(node){ return node; }) -} - Romo.prototype.nonInputTextKeyCodes = function() { // https://css-tricks.com/snippets/javascript/javascript-keycodes/ return [ 9, /* tab */ 13, /* enter */ @@ -601,13 +837,17 @@ Romo.prototype._eid = 1; Romo.prototype._fid = 1; Romo.prototype._el = function(elem) { - elem._romoeid || ( - elem._romoeid = (this.attr(elem, 'data-romo-eid') || this.setAttr(elem, 'data-romo-eid', this._eid++)) - ); + if (!elem._romoeid) { + if (elem !== window && elem !== document) { + elem._romoeid = (this.data(elem, 'romo-eid') || this.setData(elem, 'romo-eid', this._eid++)); + } else { + elem._romoeid = elem === window ? 'window' : 'document'; + } + } return elem; } Romo.prototype._fn = function(fn) { fn._romofid || (fn._romofid = this._fid++); @@ -620,81 +860,268 @@ return 'eid--'+this._el(elem)._romoeid+'--'+eventName+'--fid--'+this._fn(fn)._romofid; } Romo.prototype._deserializeValue = function(value) { try { - if (value === "true") { return true; } // "true" => true - if (value === "false") { return false; } // "false" => false - if (value === "undefined") { return undefined; } // "undefined" => undefined - if (value === "null") { return null; } // "null" => null - if (+value+"" === value) { return +value; } // "42.5" => 42.5 - if (/^[\[\{]/.test(value)) { JSON.parse(value); } // JSON => parse if valid - return value; // String => self + if (value === "true") { return true; } // "true" => true + if (value === "false") { return false; } // "false" => false + if (value === "undefined") { return undefined; } // "undefined" => undefined + if (value === "null") { return null; } // "null" => null + if (value === null) { return undefined; } // null => undefined + if (+value+"" === value) { return +value; } // "42.5" => 42.5 + if (/^[\[\{]/.test(value)) { return JSON.parse(value); } // JSON => parse if valid + return value; // String => self } catch(e) { return value } } -// TODO: rework w/o jQuery -Romo.prototype._addEventCallback = function(name, callback) { - this._eventCallbacks.push({ eventName: name, callback: callback }); +Romo.prototype._overflowScrollableRegex = /(auto|scroll)/; +Romo.prototype._elemsTagNameRegEx = /<([a-z0-9-]+)[\s\/>]+/i; + +Romo.prototype._elemsWrapMap = { + 'caption': [1, "<table>", "</table>"], + 'colgroup': [1, "<table>", "</table>"], + 'col': [2, "<table><colgroup>", "</colgroup></table>"], + 'thead': [1, "<table>", "</table>"], + 'tbody': [1, "<table>", "</table>"], + 'tfoot': [1, "<table>", "</table>"], + 'tr': [2, "<table><tbody>", "</tbody></table>"], + 'th': [3, "<table><tbody><tr>", "</tr></tbody></table>"], + 'td': [3, "<table><tbody><tr>", "</tr></tbody></table>"] +}; + +Romo.prototype._elemsInitComponents = {}; + +Romo.prototype._elemsInitTrigger = function(onElems) { + for (var selector in this._elemsInitComponents) { + var componentClass = this._elemsInitComponents[selector]; + this._elemsInitFind(onElems, selector).forEach(function(initElem){ new componentClass(initElem); }); + } + return onElems; } -// RomoParentChildElems (TODO: rework w/o jQuery) +Romo.prototype._elemsInitFind = function(onElems, selector) { + var elems = onElems.filter(function(onElem){ return Romo.is(onElem, selector); }); + return elems.concat(Romo.find(onElems, selector));; +} +// RomoComponent + +var RomoComponent = function(constructorFn) { + var component = function() { + RomoComponent.addEventFunctions(this); + constructorFn.apply(this, arguments); + } + component.prototype.romoEvFn = {}; + component.prototype.doInit = function() {} // override as needed + return component; +} + +RomoComponent.addEventFunctions = function(klassInstance) { + for(var name in klassInstance.romoEvFn) { + klassInstance[name] = RomoComponent.eventProxyFn(klassInstance.romoEvFn[name]); + } +} + +RomoComponent.eventProxyFn = function(fn) { + return function(){ return fn.apply(this, arguments); }; +} + +// RomoPopupStack + +var RomoPopupStack = function() { + this.popupSelector = undefined; + this.styleClasses = []; + this.items = []; + + this._buildItemClass(); +} + +RomoPopupStack.prototype.doInit = function(styleClass) { + this.bodyElem = Romo.f('body')[0]; + Romo.on(this.bodyElem, 'click', Romo.proxy(this._onBodyClick, this)); + Romo.on(this.bodyElem, 'keyup', Romo.proxy(this._onBodyKeyUp, this)); + Romo.on(window, 'resize', Romo.proxy(this._onWindowResize, this)); + Romo.on(window, 'scroll', Romo.proxy(this._onWindowScroll, this)); +} + +RomoPopupStack.prototype.addStyleClass = function(styleClass) { + this.styleClasses.push(styleClass); + this.popupSelector = this.styleClasses.map(function(s){ return '.'+s; }).join(', '); +} + +RomoPopupStack.prototype.addElem = function(popupElem, boundOpenFn, boundCloseFn, boundPlaceFn) { + this.items.push(new this.itemClass(popupElem, boundCloseFn, boundPlaceFn)); + + // allow any body click events to propagate and run first. This ensures + // any existing stack is in the appropriate state before opening a new popup. + Romo.pushFn(boundOpenFn); +} + +RomoPopupStack.prototype.closeThru = function(popupElem) { + // allow any body click events to propagate and run first. This ensures + // any existing stack is in the appropriate state before opening a new popup. + Romo.pushFn(Romo.proxy(function() { + if (this._includes(popupElem)) { + this.closeTo(popupElem); + this._closeTop(); + } + }, this)); +} + +RomoPopupStack.prototype.closeTo = function(popupElem) { + if (this._includes(popupElem)) { + while (this.items.length > 0 && !this.items[this.items.length-1].isFor(popupElem)) { + this._closeTop(); + } + } +} + +RomoPopupStack.prototype.placeAllPopups = function(includingFixed) { + this.items.filter(function(item) { + return includingFixed || Romo.css(item.popupElem, 'position') !== 'fixed'; + }).forEach(function(item){ + item.placeFn(); + }); +} + +// private + +RomoPopupStack.prototype._buildItemClass = function() { + this.itemClass = function(popupElem, closeFn, placeFn) { + this.popupElem = popupElem; + this.closeFn = closeFn; + this.placeFn = placeFn; + } + this.itemClass.prototype.isFor = function(popupElem) { + return this.popupElem === popupElem; + } +} + +RomoPopupStack.prototype._closeTop = function() { + var item; + if (this.items.length > 0) { + item = this.items.pop(); + item.closeFn(); + Romo.trigger(this.bodyElem, 'romoPopupStack:popupClose', [item.popupElem, this]); + Romo.trigger(item.popupElem, 'romoPopupStack:popupClose', [this]); + } + return item; +} + +RomoPopupStack.prototype._closeAll = function() { + while (this.items.length > 0) { + this._closeTop(); + } +} + +RomoPopupStack.prototype._includes = function(popupElem) { + return this.items.reduce(function(included, item) { + return included || item.isFor(popupElem); + }, false); +} + +RomoPopupStack.prototype._onBodyClick = function(e) { + var popupElem = undefined; + if (Romo.is(e.target, this.popupSelector)) { + popupElem = e.target; + } else { + popupElem = Romo.parents(e.target, this.popupSelector)[0]; + } + + if (popupElem === undefined || !this._includes(popupElem)) { + this._closeAll(); + } else { + this.closeTo(popupElem); + } +} + +RomoPopupStack.prototype._onBodyKeyUp = function(e) { + var closedItem; + if (e.keyCode === 27 /* Esc */) { + closedItem = this._closeTop(); + if (closedItem) { + Romo.trigger(closedItem.popupElem, 'romoPopupStack:popupClosedByEsc', [this]); + } + } +} + +RomoPopupStack.prototype._onWindowResize = function(e) { + this.placeAllPopups(true); +} + +RomoPopupStack.prototype._onWindowScroll = function(e) { + this.placeAllPopups(); +} + +// RomoParentChildElems + var RomoParentChildElems = function() { this.attrName = 'romo-parent-elem-id'; this.elemId = 0; this.elems = {}; +} - var parentRemovedObserver = new MutationObserver($.proxy(function(mutationRecords) { - mutationRecords.forEach($.proxy(function(mutationRecord) { +RomoParentChildElems.prototype.doInit = function(parentElem, childElems) { + var parentRemovedObserver = new MutationObserver(Romo.proxy(function(mutationRecords) { + mutationRecords.forEach(Romo.proxy(function(mutationRecord) { if (mutationRecord.type === 'childList' && mutationRecord.removedNodes.length > 0) { - $.each($(mutationRecord.removedNodes), $.proxy(function(idx, node) { - this.remove($(node)); + mutationRecord.removedNodes.forEach(Romo.proxy(function(removedNode) { + this.remove(removedNode); }, this)); } }, this)); }, this)); - parentRemovedObserver.observe($('body')[0], { + parentRemovedObserver.observe(Romo.f('body')[0], { childList: true, subtree: true }); } RomoParentChildElems.prototype.add = function(parentElem, childElems) { - parentElem.attr('data-'+this.attrName, this._push(childElems, parentElem.data(this.attrName))); + // delay adding b/c the parent elem may be manipulated in the DOM resulting in the parent elem + // being removed and then re-added to the DOM. if the child elems are associated immediately, + // any "remove" from DOM manipulation would incorrectly remove the popup. + Romo.pushFn(Romo.proxy(function() { + Romo.setData(parentElem, this.attrName, this._push(childElems, Romo.data(parentElem, this.attrName))); + }, this)); } -RomoParentChildElems.prototype.remove = function(nodeElem) { - if (nodeElem.data('romo-parent-removed-observer-disabled') !== true) { - if (nodeElem.data(this.attrName) !== undefined) { - this._removeChildElems(nodeElem); // node is a parent elem itself +RomoParentChildElems.prototype.remove = function(elemNode) { + if (elemNode.nodeType !== Node.ELEMENT_NODE){ return false; } + + if (Romo.data(elemNode, 'romo-parent-removed-observer-disabled') !== true) { + if (Romo.data(elemNode, this.attrName) !== undefined) { + // node is a parent elem itself + this._removeChildElems(elemNode); } - $.each(nodeElem.find('[data-'+this.attrName+']'), $.proxy(function(idx, parent) { - this._removeChildElems($(parent)); + Romo.find(elemNode, '[data-'+this.attrName+']').forEach(Romo.proxy(function(childParentElem) { + this._removeChildElems(childParentElem); }, this)); } } -// private RomoParentChildElems +// private RomoParentChildElems.prototype._removeChildElems = function(parentElem) { - $.each(this._pop(parentElem.data(this.attrName)), function(idx, elem) { - $(elem).remove(); + this._pop(Romo.data(parentElem, this.attrName)).forEach(function(childElem) { + Romo.remove(childElem); + Romo.trigger(childElem, 'romoParentChildElems:childRemoved', [childElem]); }); }; RomoParentChildElems.prototype._push = function(items, id) { if (id === undefined) { id = String(this.elemId++); } if (this.elems[id] === undefined) { this.elems[id] = [] } - items.forEach($.proxy(function(item){ this.elems[id].push(item) }, this)); + this.elems[id] = this.elems[id].concat(items); + return id; }; RomoParentChildElems.prototype._pop = function(id) { var items = this.elems[id]; @@ -702,11 +1129,9 @@ return items || []; } // Init -window.Romo = new Romo(); - -// TODO: rework w/o jQuery -$(function() { +var Romo = new Romo(); +Romo.ready(function() { Romo.doInit(); -}) +});