toloframework
Version:
Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.
564 lines (521 loc) • 16.6 kB
JavaScript
/**
* @module dom
*
* @description
* Functions which facilitate DOM manipulations.
*
* @example
* var mod = require('dom');
*/
require("polyfill.classList");
const
Theme = require("dom.theme"),
Gestures = require("tfw.gestures");
const
// Used to store data on the DOM element without colliding with existing attributes.
SYMBOL = '@dom' + Date.now(),
RX_ENTITY = /^&(#[0-9]+|[a-zA-Z0-9]+);$/;
function $(dom) {
if (dom instanceof Node) return dom;
if (dom === undefined || dom === null) {
throw Error("`dom` is not a valid element!", dom);
}
if (dom.$ instanceof Node) return dom.$;
if (dom.element instanceof Node) return dom.element;
if (typeof dom === 'string') {
var elem = document.getElementById(dom);
if (!elem) {
console.error("[dom] There is no DOM element with this ID: `" + dom + "`");
}
return elem;
}
if (typeof dom.element === 'function') return dom.element();
return dom;
};
module.exports = $;
$.tagNS = tagNS;
$.svgRoot = tagNS.bind(undefined, "http://www.w3.org/2000/svg", "svg", {
version: '1.1',
'xmlns:svg': 'http://www.w3.org/2000/svg',
xmlns: 'http://www.w3.org/2000/svg',
'xmlns:xlink': 'http://www.w3.org/1999/xlink'
});
$.svg = tagNS.bind(undefined, "http://www.w3.org/2000/svg");
$.tag = tagNS.bind(undefined, "http://www.w3.org/1999/xhtml");
$.div = tagNS.bind(undefined, "http://www.w3.org/1999/xhtml", "div");
$.txt = window.document.createTextNode.bind(window.document);
$.textOrHtml = textOrHtml;
$.get = get;
/**
* Add a readonly `element` property to `obj` and return it.
*/
$.elem = elem;
/**
* Apply css rules on `element`.
*
* @return `element`.
*
* @example
* var $ = require('dom');
* $.css( element, { width: '800px'. height: '600px' });
*/
$.css = css;
$.att = att;
$.removeAtt = removeAtt;
$.addClass = addClass;
$.hasClass = hasClass;
$.removeClass = removeClass;
$.toggleClass = toggleClass;
$.saveStyle = saveStyle;
$.restoreStyle = restoreStyle;
/**
* @param {string} name - Name of this theme.
* @param {object} style - Colors.
* @param {string} style.bg0 - [optional] Background level 0.
* @param {string} style.bg1 - [optional] Background level 1.
* @param {string} style.bg2 - [optional] Background level 2.
* @param {string} style.bg3 - [optional] Background level 3.
* @param {string} style.bgP - [optional] Primary color.
* @param {string} style.bgPD - [optional] Primary color (dark version).
* @param {string} style.bgPL - [optional] Primary color (light version).
* @param {string} style.bgS - [optional] Accent color.
* @param {string} style.bgSD - [optional] Accent color (dark version).
* @param {string} style.bgSL - [optional] Accent color (light version).
* @param {string} style.fg* - [optional] To each background color
* (bg) correspond a text color (fg).
*/
$.registerTheme = Theme.register.bind($);
/**
* @param {string} name - Name of the heme to apply.
*/
$.applyTheme = Theme.apply.bind($);
$.registerAndApplyTheme = registerAndApplyTheme;
/**
* @param newElem {Element} - Replacement element.
* @param oldElem {Element} - Element to replace.
*/
$.replace = replace;
/**
* Remove element from its parent.
* @param element {Element} - Element to detach from its parent.
* @return The parent element.
*/
$.detach = detach;
/**
* Add event handlers to one or many elements.
*
* @param {object|array} element - list of elements on which apply
* events handlers.
*
* @param {object|function} slots - If a function is given, it is
* considered as a slot for the event `tap`. Otherwise, the object is
* a map between events' names (the key) and function to handle the
* event (the value).
* Events' names are:
* * __tap__: When the element is pressed and released in less than
900 ms and without too much sliding.
* * __doubletap__
* * __dragmove__
*
* @param {boolean} capture - If `true` events are captured before they reach the children.
*
* @example
* DOM.on( [screen, button], function() {...} );
* DOM.on( body, null ); // Do nothing, but stop propagation.
* DOM.on( element, { tap: function() {...} } );
*/
$.on = on;
$.off = off;
/**
* Append all the `children` to `element`.
* @param element
* @param ...children
*/
$.add = add;
/**
* Add the attribute `element` and the following functions to `obj`:
* * __css__
* * __addClass__
* * __removeClass__
* * __toggleClass__
*/
$.wrap = wrap;
/**
* Remove all children of the `element`.
* @param element {Element} - Element from which remove all the children.
*/
$.clear = clear;
function wrap(obj, element, nomethods) {
Object.defineProperty(obj, 'element', {
value: element,
writable: false,
configurable: false,
enumerable: true
});
if (nomethods) return obj;
obj.on = on.bind(obj, element);
obj.css = css.bind(obj, element);
obj.add = add.bind(obj, element);
obj.att = att.bind(obj, element);
obj.addClass = addClass.bind(obj, element);
obj.hasClass = hasClass.bind(obj, element);
obj.removeClass = removeClass.bind(obj, element);
obj.toggleClass = toggleClass.bind(obj, element);
return obj;
}
function replace(newElem, oldElem) {
newElem = $(newElem);
oldElem = $(oldElem);
oldElem.parentNode.replaceChild(newElem, oldElem);
return newElem;
}
function css(element, styles) {
element = $(element);
var key, val;
for (key in styles) {
val = styles[key];
element.style[key] = val;
}
return element;
}
function att(element, attribs, value) {
element = $(element);
var key, val;
if (typeof attribs === 'string') {
if (typeof value === 'undefined') value = "";
key = attribs;
attribs = {};
attribs[key] = value;
}
for (key in attribs) {
val = attribs[key];
element.setAttribute(key, val);
}
return element;
}
function removeAtt(element, attrib) {
element = $(element);
element.removeAttribute(attrib);
return element;
}
function add(element) {
element = $(element);
try {
var i, child, isValidArgument;
for (i = 1; i < arguments.length; i++) {
child = arguments[i];
isValidArgument = addArray(element, child) ||
addText(element, child) ||
addNode(element, child);
if (!isValidArgument) {
console.error("Argument #" + i + " of dom.add() is invalid!", arguments);
debugger;
}
}
return element;
} catch (ex) {
console.error("[DOM.add] arguments=", [].slice.call(arguments));
console.error("[DOM.add] exception=", ex);
}
}
function addNode(element, child) {
child = $(child);
if (child instanceof Node) {
element.appendChild(child);
return true;
}
return false;
}
function addText(element, child) {
if (typeof child === 'number') child = '' + child;
if (typeof child !== 'string') return false;
if (child.substr(0, 6).toLowerCase() == '<html>') {
// Text starting with '<html>' is HTML code to parse.
var html = child.substr(6);
child = $.tag('span');
child.innerHTML = html;
} else if (RX_ENTITY.test(child)) {
// HTML entity, for instance: &, <, ...
var text = child;
child = $.tag('span');
child.innerHTML = text;
} else {
child = document.createTextNode(child);
}
element.appendChild(child);
return true;
}
function addArray(element, array) {
if (!Array.isArray(array)) return false;
array.forEach(function(child) {
add(element, child);
});
return true;
}
function off(element) {
if (Array.isArray(element)) {
element.forEach(function(elem) {
off(elem);
});
return element;
}
if (typeof element[SYMBOL] === 'undefined') return element;
var pe = element[SYMBOL].events;
if (typeof pe === 'undefined') return element;
pe.off();
delete element[SYMBOL].events;
}
var NON_POINTER_EVENTS = [
'keyup', 'keydown', 'scroll', 'load', 'error'
];
function on(element, slots, extra) {
// If only a function is passed, we consider this is a Tap event.
if (typeof slots === 'function' || slots === null) slots = { tap: slots };
else if (typeof slots === 'string' && typeof extra === 'function') {
// Syntax: $.on( elem, 'tap', function(evt) {...} )
var obj = {};
obj[slots] = extra;
slots = obj;
}
if (Array.isArray(element)) {
element.forEach(function(elem) {
on(elem, slots);
});
return element;
}
element = $(element);
if (typeof element[SYMBOL] === 'undefined') element[SYMBOL] = {};
if (typeof element[SYMBOL].events === 'undefined') {
element[SYMBOL].events = Gestures(element);
}
var key, val, preview;
for (key in slots) {
val = slots[key];
if (key.charAt(0) == '!') {
key = key.substr(1);
preview = true;
} else {
preview = false;
}
if (NON_POINTER_EVENTS.indexOf(key.toLowerCase()) > -1) {
element.addEventListener(key, val, preview);
} else {
element[SYMBOL].events.on(key, val, preview);
}
}
return element;
}
function tagNS(ns, name) {
try {
var e = document.createElementNS(ns, name.trim().toLowerCase());
var i, arg, key, val;
for (i = 2; i < arguments.length; i++) {
arg = arguments[i];
if (Array.isArray(arg)) {
// Arrays are for children.
arg.forEach(function(child) {
switch (typeof child) {
case 'string':
case 'number':
case 'boolean':
child = '' + child;
if (child.substr(0, 6) == '<html>') {
var html = child.substr(6);
child = $.tag('span');
child.innerHTML = html;
} else {
child = document.createTextNode(child);
}
break;
}
add(e, child);
});
} else {
switch (typeof arg) {
case "string":
arg.split(' ').forEach(function(item) {
if (item.length > 0) {
addClass(e, item);
}
});
break;
case "object":
for (key in arg) {
val = arg[key];
e.setAttribute(key, val);
}
break;
default:
throw Error("[dom.tag] Error creating <" + name + ">: Invalid argument #" + i + "!");
}
}
}
return e;
} catch (ex) {
console.error("[dom.tagNS] Error with `ns` = ", ns, " and `name` = ", name);
console.error(ex);
}
};
function addClass(elem) {
var args = [].slice.call(arguments, 1);
if (Array.isArray(elem)) {
// Loop on each element.
args.unshift(null);
elem.forEach(function(child) {
args[0] = child;
addClass.apply(undefined, args);
});
return elem;
}
elem = $(elem);
args.forEach(function(className) {
if (typeof className !== 'string') return;
className = className.trim();
if (className.length == 0) return;
try {
if (elem.classList)
elem.classList.add(className);
} catch (ex) {
console.error("[dom.addClass] Invalid class name: ", className);
console.error(ex);
}
});
return elem;
}
function hasClass(elem, className) {
elem = $(elem);
if (!elem.classList) return false;
return elem.classList.contains(className);
}
function removeClass(elem) {
var args = [].slice.call(arguments, 1);
if (Array.isArray(elem)) {
// Loop on each element.
args.unshift(null);
elem.forEach(function(child) {
args[0] = child;
removeClass.apply(undefined, args);
});
return elem;
}
elem = $(elem);
args.forEach(function(className) {
if (typeof className !== 'string') return;
try {
if (elem.classList)
elem.classList.remove(className);
} catch (ex) {
console.error("[dom.removeClass] Invalid class name: ", className);
console.error(ex);
}
});
return elem;
}
function toggleClass(elem) {
var args = [].slice.call(arguments, 1);
args.forEach(function(className) {
if (hasClass(elem, className)) {
removeClass(elem, className);
} else {
addClass(elem, className);
}
});
return elem;
}
function clear(element) {
// (!) On préfère retirer les éléments un par un du DOM plutôt que d'utiliser simplement
// this.html("").
// En effet, le code simplifié a des conséquences inattendues dans IE9 et IE10 au moins.
// Le bug des markers qui disparaissaients sur les cartes de Trail-Passion 4 a été corrigé
// avec cette modification.
element = $(element);
var e = element;
while (e.firstChild) {
e.removeChild(e.firstChild);
}
var args = [].slice.call(arguments);
if (args.length > 1) {
add.apply(this, args);
}
return element;
}
function get(element, query) {
element = $(element);
if (typeof query === 'undefined') {
query = element;
element = window.document;
}
return element.querySelector(query);
}
function detach(element) {
element = $(element);
var parent = element.parentElement;
if (!parent) return parent;
parent.removeChild(element);
return parent;
}
function elem(target) {
var args = [].slice.call(arguments);
args.shift();
if (args.length == 0) args = ['div'];
args.push('dom', 'custom');
var e;
if (typeof args[0].element !== 'undefined') {
e = args[0].element;
addClass(e, 'dom', 'custom');
} else if (typeof args[0].appendChild === 'function') {
e = args[0];
addClass(e, 'dom', 'custom');
} else {
e = $.tag.apply($, args);
}
Object.defineProperty(target, 'element', {
value: e,
writable: false,
configurable: false,
enumerable: true
});
return e;
}
function textOrHtml(element, content) {
if (typeof content === 'undefined') content = '';
if (content === null) content = '';
if (typeof content !== 'string') content = JSON.stringify(content);
if (content.substr(0, 6) == '<html>') {
element.innerHTML = content.substr(6);
} else {
element.textContent = content;
}
return element;
}
function saveStyle(elements) {
if (!Array.isArray(elements)) return saveStyle(Array.prototype.slice.call(arguments));
elements.forEach(function(elem) {
elem = $(elem);
if (typeof elem[SYMBOL] === 'undefined') elem[SYMBOL] = {};
if (!Array.isArray(elem[SYMBOL].style)) elem[SYMBOL].style = [];
elem[SYMBOL].style.push(JSON.stringify(elem.style));
});
}
function restoreStyle(elements) {
if (!Array.isArray(elements)) return restoreStyle(Array.prototype.slice.call(arguments));
elements.forEach(function(elem) {
elem = $(elem);
if (typeof elem[SYMBOL] === 'undefined' || !Array.isArray(elem[SYMBOL].style)) throw Error(
"[dom.restoreStyle] `saveStyle()` has never been used on this element!");
if (elem[SYMBOL].style.length == 0) throw Error(
"[dom.restoreStyle] more `restore` than `save`!");
var styles = JSON.parse(elem[SYMBOL].style.pop());
var k, v;
for (k in styles) {
v = styles[k];
if (typeof v !== 'undefined') {
elem.style[k] = v;
}
}
});
};
function registerAndApplyTheme(name, styles) {
Theme.register.call(this, name, styles);
Theme.apply.call(this, name);
}