src/dom/element.js
/**
* @use /demand/validator/isObject
* @use /demand/validator/isInstanceOf
* @use /demand/validator/isTypeOf
* @use /demand/function/uuid
* @use /demand/function/iterate
*
* @require ./event
* @require ../hooks/css
* @require ../support/method
*/
(function(global, document) {
'use strict';
function definition(isObject, isInstanceOf, isTypeOf, generateUuid, iterate, Event, hooksCss, supportMethod) {
var //shortcuts
documentBody = document.body || document.getElementsByTagName('body')[0],
arrayPrototypeConcat = Array.prototype.concat,
arrayPrototypeSlice = Array.prototype.slice,
objectDefineProperty = Object.defineProperty,
objectDefineProperties = Object.defineProperties,
head = document.getElementsByTagName('head')[0],
// constants
NULL = null,
STRING_UNDEFINED = 'undefined',
STRING_STRING = 'string',
STRING_CONTENTATTRIBUTE = ('textContent' in document.createElement('a')) ? 'textContent' : 'innerText',
STRING_MATCHES = supportMethod('matches', documentBody) || supportMethod('matchesSelector', documentBody),
// regular expressions
regexMatchTag = /^<(\w+)\s*\/>$/,
regexMatchSpaces = / +/g,
regexMatchChildSeclector = /^\s*^/,
// methods
previousSibling = (!isTypeOf(head.previousElementSibling, STRING_UNDEFINED)) ? function previousSibling() { return this.previousElementSibling; } : function previousSibling() { var element = this; while(element = element.previousSibling) { if(element.nodeType === 1 ) { return element; } } },
nextSibling = (!isTypeOf(head.nextElementSibling, STRING_UNDEFINED)) ? function nextSibling() { return this.nextElementSibling; } : function nextSibling() { var element = this; while(element = element.nextSibling) { if(element.nodeType === 1 ) { return element; } } },
// flags
// general storage & objects
listener = {},
supportsPassiveListener = false;
(function() {
var noop = function() {},
event = '' + (+new Date()),
options;
try {
options = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassiveListener = true;
}
});
global.addEventListener(event, noop, options);
global.removeEventListener(event, noop, options);
} catch(error) {} /* eslint-disable-line no-empty */
}());
function processOptions(options) {
if(supportsPassiveListener || typeof options === 'boolean') {
return options;
}
return options && options.capture;
}
function emitEvent(type, detail) {
var self = this,
event;
event = document.createEvent('CustomEvent');
event.initCustomEvent(type, type !== 'load' && type !== 'resize', type !== 'load' && type !== 'resize', detail);
self.node.dispatchEvent(event);
}
function resolveElement(element) {
if(typeof element === 'string') {
try {
if(regexMatchTag.test(element)) {
element = document.createElement(element.replace(regexMatchTag, '$1').toLowerCase());
} else {
element = document.querySelector(element);
}
} catch(exception) {
element = NULL;
}
}
if(!element) {
throw new Error('Element could not be resolved');
}
return element;
}
function resolveArguments(parameters) {
return arrayPrototypeConcat.apply([], arrayPrototypeSlice.call(parameters)).join(' ').split(regexMatchSpaces);
}
function matchesSelector(event, selector) {
var i = 0, pointer;
for(; pointer = event.path[i]; i++) {
if(pointer[STRING_MATCHES] && pointer[STRING_MATCHES](selector)) {
event.isDelegate = true;
event.currentTarget = pointer;
return true;
}
if(pointer === event.currentTarget) {
break;
}
}
return false;
}
function getSiblings(pointer, method, selector, limit, strict) {
var multiple = !(limit && limit === 1),
siblings = multiple ? [] : false;
strict = multiple ? false : strict;
while(pointer = method.call(pointer)) {
if(pointer.nodeType === 1) {
if(!selector || pointer[STRING_MATCHES](selector)) {
if(multiple) {
siblings.push(pointer);
} else {
return pointer;
}
}
if(strict) {
break;
}
}
}
return siblings;
}
function getParents(pointer, selector, limit, strict) {
var multiple = !(limit && limit === 1),
parents = multiple ? [] : false;
strict = multiple ? false : strict;
while(pointer = pointer.parentNode) {
if(pointer.nodeType === 1) {
if(!selector || pointer[STRING_MATCHES](selector)) {
if(multiple) {
parents.push(pointer);
} else {
return pointer;
}
}
if(strict) {
break;
}
}
}
return parents
}
function DomElement(element, attributes, styles) {
var self = this,
uuid;
element = resolveElement(element);
uuid = element.uuid;
if(!uuid) {
uuid = generateUuid();
listener[uuid] = {};
objectDefineProperty(element, 'uuid', { value: uuid });
}
objectDefineProperties(self, {
uuid: { value: uuid },
type: { value: element === global ? '#window' : element.nodeName },
node: { value: element }
});
if(isObject(attributes)) {
self.setAttributes(attributes);
}
if(isObject(styles)) {
self.setStyles(styles);
}
return self;
}
DomElement.prototype = {
/* only for reference
uuid: NULL,
type: NULL,
node: NULL
*/
clone: function() {
return new DomElement(this.node.cloneNode(true));
},
focus: function() {
this.node.focus();
return this;
},
blur: function() {
this.node.blur();
return this;
},
getPosition: function() {
var bbox = this.node.getBoundingClientRect();
return {
left: bbox.left + documentBody.scrollLeft,
top: bbox.top + documentBody.scrollTop
};
},
getOffset: function(viewport) {
var node = this.node,
bbox = viewport ? node.getBoundingClientRect() : null;
return {
left: viewport ? bbox.left : node.offsetLeft,
top: viewport ? bbox.top : node.offsetTop
};
},
getWidth: function(includeMargin) {
var node = this.node,
width = node.offsetWidth,
style;
if(includeMargin) {
style = getComputedStyle(node);
width += parseInt(style.marginLeft) + parseInt(style.marginRight);
}
return width;
},
getHeight: function(includeMargin) {
var node = this.node,
height = node.offsetHeight,
style;
if(includeMargin) {
style = getComputedStyle(node);
height += parseInt(style.marginTop) + parseInt(style.marginBottom);
}
return height;
},
getContent: function(getHtml) {
var node = this.node;
return getHtml ? node.innerHTML : node[STRING_CONTENTATTRIBUTE];
},
getAttribute: function(attribute) {
var self = this;
if(isTypeOf(attribute, STRING_STRING)) {
return self.node.getAttribute(attribute);
}
},
getAttributes: function() {
var self = this,
result = {},
attributes = resolveArguments(arguments),
i = 0, attribute;
for(; attribute = attributes[i]; i++) {
result[attribute] = self.node.getAttribute(attribute);
}
return result;
},
getStyle: function(property) {
var self = this;
if(isTypeOf(property, STRING_STRING)) {
return hooksCss.process('get', self.node, property);
}
},
getStyles: function() {
var self = this,
result = {},
properties = resolveArguments(arguments),
i = 0, property;
for(; property = properties[i]; i++) {
result[property] = hooksCss.process('get', self.node, property);
}
return result;
},
getSiblingBefore: function(selector, strict) {
return getSiblings(this.node, previousSibling, selector, 1, strict);
},
getSiblingAfter: function(selector, strict) {
return getSiblings(this.node, nextSibling, selector, 1, strict);
},
getSiblings: function(selector) {
return this.getSiblingsBefore(selector).concat(this.getSiblingsAfter(selector));
},
getSiblingsBefore: function(selector) {
return getSiblings(this.node, previousSibling, selector);
},
getSiblingsAfter: function(selector) {
return getSiblings(this.node, nextSibling, selector);
},
getChildren: function(selector) {
var self = this.node,
uuid, matches, i, match;
if(!selector) {
matches = [];
for(i = 0; match = self.childNodes[i]; i++) {
if(match.nodeType === 1) {
matches.push(match);
}
}
} else if(regexMatchChildSeclector.test(selector)) {
uuid = self.uuid;
self.setAttribute('nucleus-uuid', uuid);
selector = '[nucleus-uuid="' + uuid + '"] ' + selector;
matches = arrayPrototypeSlice.call(self.parentNode.querySelectorAll(selector));
self.removeAttribute('nucleus-uuid');
} else {
matches = arrayPrototypeSlice.call(self.querySelectorAll(selector));
}
return matches;
},
getParent: function(selector, strict) {
return getParents(this.node, selector, 1, strict);
},
getParents: function(selector) {
return getParents(this.node, selector);
},
hasChild: function(child) {
var node = this.node;
return node !== child && node.contains(child.node || child);
},
hasClass: function(name) {
return (name) ? (new RegExp('(?:^|\\s)' + name + '(?:\\s|$)')).test(this.node.className) : false;
},
isVisible: function() {
var self = this,
node = self.node;
return !((node.offsetWidth <= 0 && node.offsetHeight <= 0) || self.getStyle('visibility') === 'hidden' || self.getStyle('opacity') <= 0);
},
setContent: function(source, isHtml) {
var self = this,
node = self.node;
if(isHtml) {
node.innerHTML = source;
} else {
node[STRING_CONTENTATTRIBUTE] = source;
}
return self;
},
setAttribute: function(attribute, value) {
var self = this;
if(isTypeOf(attribute, STRING_STRING)) {
self.node.setAttribute(attribute, value);
}
return self;
},
setAttributes: function(attributes) {
var self = this;
iterate(attributes, function(attribute, value) {
self.setAttribute(attribute, value);
});
return self;
},
removeAttribute: function(attribute) {
var self = this;
if(isTypeOf(attribute, STRING_STRING)) {
self.node.removeAttribute(attribute);
}
return self;
},
removeAttributes: function() {
var self = this,
attributes = resolveArguments(arguments),
i = 0, attribute;
for(; attribute = attributes[i]; i++) {
self.removeAttribute(attribute);
}
return self;
},
setStyle: function(property, value) {
var self = this;
if(isTypeOf(property, STRING_STRING)) {
hooksCss.process('set', self.node, property, value);
}
return self;
},
setStyles: function(properties) {
var self = this;
iterate(properties, function(property, value) {
hooksCss.process('set', self.node, property, value);
});
return self;
},
removeStyle: function(property) {
var self = this;
if(isTypeOf(property, STRING_STRING)) {
self.setStyle(property, '');
}
return self;
},
removeStyles: function() {
var self = this,
properties = resolveArguments(arguments),
i = 0, property;
for(; property = properties[i]; i++) {
self.setStyle(property, '');
}
return self;
},
addClass: function(name) {
var self = this;
if(name && !self.hasClass(name)) {
self.node.className += (self.node.className) ? ' ' + name : name;
}
return self;
},
removeClass: function(name) {
var self = this;
if(name && self.hasClass(name)) {
self.node.className = self.node.className.replace(new RegExp('(?:^|\\s)' + name + '(?!\\S)'), '').trim();
}
return self;
},
toggleClass: function(name) {
var self = this;
if(name) {
self.hasClass(name) ? self.removeClass(name) : self.addClass(name);
}
return self;
},
prepend: function(element) {
var self = this,
target = self.node;
if(element) {
try {
element = (isInstanceOf(element, DomElement)) ? element.node : resolveElement(element);
target.firstChild ? target.insertBefore(element, target.firstChild) : self.append(element);
} catch(exception) {
target.insertAdjacentHTML('afterBegin', element);
}
}
return self;
},
append: function(element) {
var self = this,
target = self.node;
if(element) {
try {
target.appendChild((isInstanceOf(element, DomElement)) ? element.node : resolveElement(element));
} catch(exception) {
target.insertAdjacentHTML('beforeEnd', element);
}
}
return self;
},
prependTo: function(target) {
var self = this,
node = self.node;
if(target) {
(target = target.node || resolveElement(target)).firstChild ? target.insertBefore(node, target.firstChild) : self.appendTo(target);
}
return self;
},
appendTo: function(target) {
var self = this;
if(target) {
(target.node || resolveElement(target)).appendChild(self.node);
}
return self;
},
insertBefore: function(target) {
var self = this,
node = self.node;
if(target) {
(target = target.node || resolveElement(target)).parentNode.insertBefore(node, target);
}
return self;
},
insertAfter: function(target) {
var self = this,
node = self.node;
if(target) {
(target = target.node || resolveElement(target)).nextSibling ? target.parentNode.insertBefore(node, target.nextSibling) : self.appendTo(target.parentNode);
}
return self;
},
replace: function(target) {
var self = this,
node = self.node;
if(target) {
(target = target.node || resolveElement(target)).parentNode.replaceChild(node, target);
}
return self;
},
replaceWith: function(element) {
var self = this,
target = self.node;
if(element) {
element = (isInstanceOf(element, DomElement)) ? element.node : resolveElement(element);
target.parentNode.replaceChild(element, target);
}
return self;
},
detach: function() {
var self = this,
node = self.node;
node.parentNode && node.parentNode.removeChild(node);
return self;
},
on: function(events) {
var self = this,
selector = (arguments.length === 4 || typeof arguments[1] === 'string') ? arguments[1] : NULL,
fn = (arguments.length === 4 || typeof arguments[2] === 'function') ? arguments[2] : arguments[1],
options = processOptions((arguments.length > 3) ? arguments[3] : arguments[2]),
uuid = fn.uuid || (fn.uuid = generateUuid()),
i = 0, event;
events = events.split(regexMatchSpaces);
for(; (event = events[i]); i++) {
var id = event + '-' + uuid,
handler = function(event) {
event = new Event(event);
if(!event.isPropagationStopped) {
event.uuid = generateUuid();
if(!selector || matchesSelector(event, selector)) {
fn.call(event.currentTarget, event, event.originalEvent.detail);
}
}
};
handler.type = event;
listener[self.uuid][id] = handler;
self.node.addEventListener(event, handler, options);
}
return self;
},
one: function(events) {
var self = this,
selector = (arguments.length === 5 || typeof arguments[1] === 'string') ? arguments[1] : NULL,
fn = (arguments.length === 5 || typeof arguments[2] === 'function') ? arguments[2] : arguments[1],
options = processOptions((arguments.length > 3) ? arguments[3] : arguments[2]),
each = ((arguments.length > 4) ? arguments[4] : arguments[3]) !== false,
handler = function(event) {
self.off(((each === true) ? event.type : events), handler, options);
fn.call(this, event, event.originalEvent.detail);
};
fn.uuid = handler.uuid = generateUuid();
if(selector) {
self.on(events, selector, handler, options);
} else {
self.on(events, handler, options);
}
return self;
},
off: function(events, fn, options) {
var self = this,
node = self.node,
i = 0, event, id, handler;
options = processOptions(options);
events = events.split(' ');
for(; event = events[i]; i++) {
id = fn.uuid && event + '-' + fn.uuid || NULL;
handler = id && listener[self.uuid][id] || NULL;
if(handler) {
node.removeEventListener(event, handler, options);
delete listener[self.uuid][id];
} else {
node.removeEventListener(event, fn, options);
}
}
return self;
},
emit: function(event, data) {
var self = this;
emitEvent.call(self, event, data);
return self;
}
};
return DomElement;
}
provide([ '/demand/validator/isObject', '/demand/validator/isInstanceOf', '/demand/validator/isTypeOf', '/demand/function/uuid', '/demand/function/iterate', './event', '../hooks/css', '../support/method' ], definition);
}(this, document));