Source: commonformfunctions.js

"use strict";

/**
 * An abstract class containing some separate potentionally universal methods used in the DT library.
 * (It is not necessary for the class to be abstract but there is no point in instantiating it.)
 * @abstract
 */
class CommonFormFunctions {
	/**
	 * Create CommonFormFunctions.
	 */
	constructor() {
		/* http://stackoverflow.com/a/30560792 */
		if(new.target === CommonFormMethods) {
			throw new TypeError("Cannot construct CommonFormFunctions instance, abstract class");
		}
		return this;
	}
	
	/**
	 * Strip a DOM element of all its children.
	 * @param {dom} element - A dom element to be emptied.
	 * @return {number} - A number of child elements deleted.
	 */
	static empty(element) {
		let counter = 0;
		while(element.hasChildNodes()) {
			element.removeChild(element.lastChild);
			counter++;
		}
		return counter;
	}
	
	/**
	 * Get DOM element's top, right, bottom and left borders, optionally counting in margins.
	 * @param {dom} element - A dom element to have the borders measured.
	 * @return {(Object|boolean)} - An object containing all DOM element positions, with keys: top, left, right, bottom - or false if no element was passed.
	 */
	static getElementPosition(element, countMargins) {
		if(element) {
			/* Get element's computed style property */
			let getStyle = function(property) {
				return window.getComputedStyle(element, null).getPropertyValue(property);
			}
			
			/* Get object with element positions {top, right, bottom, left} */
			let getPosition = function(element) {
				/* https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect */
				return element.getBoundingClientRect();
			}
			
			let marginLeftRight = 0;
			let marginTopBottom = 0;
			if(countMargins === true) {
				/* Compute Element margins */
				marginLeftRight = parseFloat(getStyle("margin-left")) + parseFloat(getStyle("margin-right"));
				marginTopBottom = parseFloat(getStyle("margin-top")) + parseFloat(getStyle("margin-bottom"));
			}
			
			return {
				top: getPosition(element).top - marginLeftRight/2,
				left: getPosition(element).left - marginTopBottom/2,
				right: getPosition(element).right + marginLeftRight/2,
				bottom: getPosition(element).bottom + marginTopBottom/2
			};
		}
		return false;
	}
	
	/**
	 * Get DOM element's top, right, bottom and left borders, counting it its margins, width and height.
	 * Requires jQuery to run!
	 * @param {dom} element - A dom element to have the borders measured.
	 * @return {(Object|boolean)} - An object containing all DOM element positions, with keys: top, left, right, bottom - or false if no element was passed.
	 */
	static getElementPositionUsingjQuery(element) {
		if(element) {
			element = $(element);
			let marginLeftRight = element.outerWidth(true) - element.width();
			let marginTopBottom = element.outerHeight(true) - element.height();
			return {
				top: element.position().top + marginLeftRight/2,
				left: element.position().left + marginTopBottom/2,
				right: element.position().left + element.innerWidth() + marginLeftRight/2,
				bottom: element.position().top + element.innerHeight() + marginTopBottom/2
			};
		}
		return false;
	}
	
	/**
	 * Append inline style to a DOM object
	 * @param {element} element - A dom element to be emptied.
	 * @param {string} style - A style to be appended.
	 * @return {boolean} - Return true if append was possible or false if element or syle is not defined.
	 */
	static appendStyle(element, style) {
		if(element && style) {
			let originalStyle = element.getAttribute("style") ? element.getAttribute("style") : "";
			element.setAttribute("style", originalStyle + style);
			return true;
		}
		return false;
	}
	
	/**
	 * Call to trigger an event on a DOM element with passed name and optionally data.
	 * @param {element} eventElement - A dom element for the event to be called on.
	 * @param {string} eventName - A name for the to-be-called event.
	 * @param {(string|Object)} [eventData] - An optional attribute containing string or object with additional data to be passed to the event.
	 * @return {boolean} - Return true if event creation was sccessful or false ig no eventElement was passed.
	 */
	static triggerEvent(eventElement, eventName, eventData) {
		eventName = eventName || "generalEvent";
		if(eventElement) {
			let event = null;
			if(eventData) {
				event = new CustomEvent(eventName, {detail: eventData});
			}
			else {
				event = new Event(eventName);
			}
			eventElement.dispatchEvent(event);
			return true;
		}
		return false;
	}
	
	/**
	 * Confirm passed input type by returning it or default to the text.
	 * Supports only the ones defined in the HTML standard.
	 * {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input|More about input tag on Mozilla Developer Network}
	 * @param {string} type - Input type to be checked.
	 * @return {string} - Return the same input type passed or default to "text" if there was no match.
	 */
	static filterProperInputTypes(type) {
		type = type || "";
		switch(type) {
			case "password":
				return "password";
			case "radio":
				return "radio";
			case "checkbox":
				return "checkbox";
			case "number":
				return "number";
			case "date":
				return "date";
			case "color":
				return "color";
			case "range":
				return "range";
			case "month":
				return "month";
			case "week":
				return "week";
			case "time":
				return "time";
			case "datetime-local":
				return "datetime-local";
			case "email":
				return "email";
			case "search":
				return "search";
			case "tel":
				return "tel";
			case "url":
				return "url";
			case "button":
				return "button";
			case "reset":
				return "reset";
			case "submit":
				return "submit";
			default:
				return "text";
		}
	}
	
	/**
	 * Join two strings together.
	 * @param {string} stringA - First string.
	 * @param {string} stringB - Second string.
	 * @param {string} [delimiter] - A delimiter to divide the two string parts. Default is comma.
	 * @return {string} - Return the two strings joined, using the default or passed delimiter.
	 */
	static joinStrings(stringA, stringB, delimiter) {
		if(typeof delimiter === undefined) {
			delimiter = ",";
		}
		
		let finalString = "";
		if(stringA) {
			finalString += stringA;
			if(stringB) {
				if(delimiter) {
					finalString += delimiter;
				}
				finalString += stringB;
			}
		}
		return finalString;
	}
	
	/**
	 * Post passed data to a specified url
	 * @param {string} url - Specify URL to submit the data to.
	 * @param {string} data - Data to be submitted.
	 * @return {Promise} - A promise for the XMLHttp submit request
	 */
	static postData(url, data) {
		return new Promise((resolve, reject) => {
			let xhr = new XMLHttpRequest();
			xhr.onreadystatechange = () => {
				if(xhr.readyState == 4) {
					if(xhr.status == 200) {
						resolve(xhr.response);
					}
					else {
						reject(Error(xhr.statusText));
					}
				}
			};
			xhr.onerror = function() {
				reject(Error("Network Error"));
			};
			xhr.open("POST", url, true);
			xhr.send(data);
		});
	}
	
	/**
	 * A generic function to make items draggable using any plugin defined.
	 * @param {Object} data - Define draggable behaviour.
	 * @param {dom} data.item - Defines the element which should be draggable.
	 * @param {dom} [data.container] - Defines the outer element in which the dragging should be contained.
	 * @param {function} data.plugin - Defines a function that should take care of draggable items.
	 * @param {function} [data.onPosChange] - Defines a function that should be executed on draggable item position change.
	 * @param {function} [data.onEnd] - Defines a function that should be executed once draggable event ends.
	 * @return {mixed} - Returns plugin function result.
	 */
	static draggableSnippet(data) {
		data = data || {};
		if(!data.item || !data.plugin) {
			throw "draggableSnippet must have draggable item and plugin defined!";
		}
		
		return data.plugin({
			item: data.item,
			container: data.container || null,
			plugin: data.plugin,
			onPosChange: data.onPosChange || null,
			onEnd: data.onEnd || null
		});
	}
	
	/**
	 * jQuery draggable plugin, requires jQuery Draggable
	 * @see {@link https://jqueryui.com/draggable/|jQuery Draggable}
	 * @param {Object} data - Define draggable behaviour.
	 * @param {dom} data.item - Defines the element which should be draggable.
	 * @param {dom} [data.container] - Defines the outer element in which the dragging should be contained.
	 * @param {function} [data.onEnd] - Defines a function that should be executed once draggable event ends.
	 */
	static draggablePlugin_jQuery(data) {
		data = data || {};
		if(!data.item) {
			throw "Plugin must have draggable item defined!";
		}
		
		if(typeof $ !== "function") {
			throw "jQuery must be loaded prior to using draggable in the DT library!";
		}
		
		if(typeof $().draggable !== "function") {
			throw "jQuery Draggable must be loaded prior to using draggable in the DT library!";
		}
		
		$(data.item).draggable({
			containment: data.container || null,
			stop: data.onEnd || null
		});
	}
	
	/**
	 * jQuery draggable plugin, requires interact.js
	 * @see {@link http://interactjs.io/|interact.js}
	 * @param {Object} data - Define draggable behaviour.
	 * @param {dom} data.item - Defines the element which should be draggable.
	 * @param {dom} [data.container] - Defines the outer element in which the dragging should be contained.
	 * @param {function} [data.onPosChange] - Defines a function that should be executed on draggable item position change.
	 * @param {function} [data.onEnd] - Defines a function that should be executed once draggable event ends.
	 */
	static draggablePlugin_interact(data) {
		data = data || {};
		if(!data.item) {
			throw "Plugin must have draggable item defined!";
		}
		
		if(typeof interact !== "function") {
			throw "interact.js must be loaded prior to using draggable in the DT library!";
		}
		
		/* source: http://interactjs.io/ */
		interact(data.item).draggable({
			// keep the element within the area of it's parent
			restrict: {
				restriction: data.container || null,
				endOnly: false,
				elementRect: { top: 0, left: 0, bottom: 1, right: 1 }	
			},

			// call this function on every dragmove event
			onmove: (event) => {
				dragMoveListener(event);
				if(typeof data.onPosChange === "function") {
					data.onPosChange();
				}
			},
			// call this function on every dragend event
			onend: data.onEnd || null
		});
		
		function dragMoveListener(event) {
			var target = event.target,
			// keep the dragged position in the data-x/data-y attributes
			x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx,
			y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;

			// translate the element
			target.style.webkitTransform =
			target.style.transform = "translate(" + x + "px, " + y + "px)";

			// update the posiion attributes
			target.setAttribute("data-x", x);
			target.setAttribute("data-y", y);
		}

		// this is used later in the resizing and gesture demos
		window.dragMoveListener = dragMoveListener;
		
		return this;
	}
}