betterelement.js

/*
 * BetterElement: Create custom HTML elements through client-side JS.
 */

/**
  * Adds a <clock> element. If the type attribute is 'time', then show
	* Date.toLocaleTimeString, and vice versa for 'date'.
	*
	* @global
	*/
function doClock() {
	var clockElement = new Element('clock');
	clockElement.toExecuteOnRead = function (index, element) {
		if (element.getAttribute('type') === 'time') {
			element.innerHTML = new Date().toLocaleTimeString();
		} else if (element.getAttribute('type') === 'date') {
			element.innerHTML = new Date().toLocaleDateString();
		}
	};
	clockElement.addAttribute('type', true, function (attribute) {
		if (attribute === 'date' || attribute === 'time') {
			return true;
		// eslint-disable-next-line no-else-return
		} else {
			return false;
		}
	});
	clockElement.readElements();
}

/**
  * Adds a <random> tag. When min and max is valid, will generate a number
	* between them.
	*
	* @global
	*/
function doRandom() {
	var randomElement = new Element('random');
	randomElement.toExecuteOnRead = function (index, element) {
		var min = Number(element.getAttribute('min'));
		var max = Number(element.getAttribute('max'));
		element.innerHTML = Math.floor((Math.random() * (max - min)) + min);
	};
	randomElement.addAttribute('min', true, Attribute.verifyPresets.number);
	randomElement.addAttribute('max', true, Attribute.verifyPresets.number);
	randomElement.readElements();
}

/**
	* An object that semi-represents an HTML attribute.
	*
	* @constructor
	* @global
	* @param {String} nameParam The name of the attribute.
	* @param {Boolean} requiredParam Weather or not the attribute is required, or if an error should be thrown if it is missing.
	* @param {Function} verifyParam A callback to be called to verify if an attribute's value is valid.
	* @param {String} valueParam The value of the attribute. Is undefined unless set.
	* @returns {Attribute} The new attribute.

	* @property {String} name The name of the attribute.
	* @property {Boolean} required Weather or not the attribute is required, or if an error should be thrown if it is missing.
	* @property {Function} verify  A callback to be called to verify if an attribute's value is valid.
	* @property {String} value The value of the attribute. Is undefined unless set.
  */
function Attribute(nameParam, requiredParam, verifyParam, valueParam) {
	this.name = nameParam;
	this.required = requiredParam || true;
	this.value = valueParam || undefined;
	this.verify = verifyParam || function () {
		return true;
	};

	return this;
}

/**
  * An object of preset verification methods.
	* @type {Object}
	* @property {Function} numbers The verification for if it is a number.
	* @property {Function} regex The verification for if it is a valid regex.
	* @memberof Attribute
	*/
Attribute.verifyPresets = {};

/**
  * Returns true if 'number' is a number.
	*
  * @param {Number|String} number The number to check
	* @returns {Boolean} The result
	*/
Attribute.verifyPresets.number = function (number) {
	return !isNaN(Number(number));
};

/**
  * Returns true if 'regex' is a valid regular expression.
	*
	* @param {String|Regex} regex The regex to check
	* @returns {Boolean} The result
	*/
Attribute.verifyPresets.regex = function (regex) {
	try {
		// The 'new RegExp()' throws a SyntaxError, so we wrap in try/catch.
		return new RegExp(regex) !== undefined;
	} catch (err) {
		if (err instanceof SyntaxError) {
			return false;
		}

		throw err;
	}
};

/**
  * A BetterElement element.
	*
	* @param {String} nameParam The name of the BetterElement (what will be inside of the <>s)
	* @global
	* @constructor
	* @returns {Element}
  */
function Element(nameParam) {
	/**
	  * The attributes of a BetterElement.
		* @type {Attribute[]}
		*/
	this.attributes = [];
	/**
	  * The name of the BetterElement. For example, in a <clock> tag
		* would be 'clock'.
		* @type {String}
		* @constant
		*/
	this.name = nameParam;
	/**
	  * A callback for when an element is read. Called once for each
		* element.
		* @type {Function}
		*
		* @param {Number} index The index of the element.
		* @param {String} element The element's contents.
		*/
	this.toExecuteOnRead = undefined;

	/**
	  * Adds an attribute to this.attributes.
		*
		* @param {Attribute|String} attribute The attribute itself, or a string for it's name.
		* @param {Boolean} required Weather or not it is required.
		* @param {Function} verify The verification function, called on each attribute's contents.
		* @param {String} value The value of the attribute. Currently unused.
		* @returns {Attribute} The finished attribute.
		*/
	this.addAttribute = function (attribute, required, verify, value) {
		if (attribute instanceof Attribute) {
			this.attributes.push(attribute);
		} else {
			this.attributes.push(new Attribute(attribute, required, verify, value));
		}
		return this.attributes[this.attributes.length - 1];
	};

	/**
	  * Deletes an attribute from this.attributes.
		*
		* @param {String} attributename The attribute name to find and delete.
		* @returns {Attribute} The deleted attribute
		*/
	this.delAttribute = function (attributename) {
		var i = 0;
		for (; this.attributes[i].name !== attributename; i += 1) { /* empty */ }
		var attribute = this.attributes[i];
		this.attributes.splice(i, 1);
		return attribute;
	};

	/**
	  * Reads all the current elements and parses them.
		*
		* <br/><b>Note</b>: Soon this may be split into seperate functions.
		*
		* @throws <b>Error</b> if readElements is run without a toExecuteOnRead
		* @throws <b>Error</b> if an attribute is missing that is marked as required.
		* @throws <b>Error</b> if attribute verification failed.
		*/
	this.readElements = function () {
		this.elements = [].slice.call(document.getElementsByTagName(this.name));
		if (this.toExecuteOnRead) {
			var _this = this;
			this.elements.forEach(function (element, index) {
				_this.attributes.forEach(function (attribute) {
					if (attribute.required && !element.getAttribute(attribute.name)) {
						throw new Error('Missing attribute ' + attribute.name + ' in element ' + index);
					}

					if (attribute.verify) {
						if (!attribute.verify(element.getAttribute(attribute.name))) {
							throw new Error('Element verification failed (type ' +
								attribute.name + ', value ' + element.getAttribute(attribute.name) + ')');
						}
					}
				});
				_this.toExecuteOnRead(index, element);
			});
		} else {
			throw new Error(this.name + ' executed readElements() without a toExecuteOnRead!');
		}
	};

	/**
		* Fetches all appropiate elements from the DOM.
		*
		* @returns {HTMLCollection}
	  */
	this.getElements = function () {
		return document.getElementsByTagName(this.name);
	};
}

/**
  * Injects BetterElement into module.exports if it exists.
	*
	* @global
	* @private
	*/
function bEinject() {
	try {
		if (module && module.exports) {
			module.exports = {
				Attribute,
				Element,
				builtins: {
					doRandom,
					doClock
				}
			};
		}
	} catch (err) {}
}

bEinject();