Source: queryitem.js

"use strict";
/* QueryItem Class */  

/**
 * This class holds query and contains InputItemWrappers and their InputItems and OptionItems for draggable queries.
 */  
class QueryItem {
	/**
	 * Create QueryItem.
	 * @param {QueryDataBlock} queryData - A QueryDataBlock instance holding all relevant query data.
	 */
	constructor(queryData) {
		if(!queryData) {
			throw new QueryItemException("No data passed when creating a new instance of QueryItem");
		}
		if(!queryData instanceof QueryDataBlock) {
			throw new QueryItemException("Data passed when creating new instance of QueryItem were not an instance of a QueryDataBlock");
		}
		
		this.queryData = queryData;
		
		this.inputWrappers = [];
		this.inputItems = [];
		this.optionItems = [];
		
		this.data = {};
		this.errorTag = null;
		
		this.optionsData = {};
		this.optionsContainer = this.data.optionsContainer || null;
		
		/* Define draggable plugin */
		this.draggable = this.queryData.draggable || null;
	}
	
	/**
	 * Check if OptionItem it is in any of the InputItems of this QueryItem.
	 * @param {OptionItem} optionItem - An OptionItem which position should be found.
	 * @param {number} [optionalMargin] - Margin to be taken taken in account while matching OptionItem's position on InputItem positions.
	 * @return {(InputItems|false)} - Return matched InputItem instance or false if none found.
	 */
	checkIfOptionItemIsInsideInput(optionItem, optionalMargin) {
		optionItem.updatePosition();
		for(let i = 0; i < this.inputItems.length; i++) {
			/* Set default margin to 0 */
			optionalMargin = optionalMargin || 0;
			
			if(this.inputItems[i] && optionItem) {
				this.inputItems[i].updatePosition();
				
				/* Element inside vertically */
				if(optionItem.position.top > this.inputItems[i].position.top &&
				optionItem.position.bottom < this.inputItems[i].position.bottom &&
				optionItem.position.top < this.inputItems[i].position.bottom &&
				optionItem.position.top > this.inputItems[i].position.top) {
					/* Element inside horizontally */
					if(optionItem.position.right > this.inputItems[i].position.left &&
					optionItem.position.right < this.inputItems[i].position.right &&
					optionItem.position.left < this.inputItems[i].position.right &&
					optionItem.position.left > this.inputItems[i].position.left) {						
						return this.inputItems[i];
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Propagate validation to the nested inputItems and return Object.
	 * @return {Object} - Object containing an array of each InputItem and a boolean that is true if this query is valid: { allAvalid: bool, inputItems: InputItem[] }.
	 */
	validate() {
		let validationData = {
			allValid: true,
			inputItems: []
		};
		
		for(let i = 0; i < this.inputWrappers.length; i++) {
			let validation = this.inputWrappers[i].validate();
			if(!validation.valid) {
				validationData.allValid = false;
			}
			validationData.inputItems.push(this.inputWrappers[i].input);
		}
		
		return validationData;
	};
	
	/**
	 * Get all nested inputItems values.
	 * @return {string[]} - An array of all InpuItem values.
	 */
	get value() {
		var values = [];
		for(let i = 0; i < this.inputItems.length; i++) {
			values.push(this.inputItems[i].inputData);
		}
		return values;
	};
	
	/**
	 * Generates dragable items. Requires options to be set in QueryItem.queryData.
	 * @return {(dom|false)} - A newly created optionsContainer dom element, wrapping each OptionItem or false if no options present for this QueryItem.
	 */
	renderOptions() {
		if(this.queryData.options) {
			this.optionsContainer = document.createElement("div");
			this.optionsContainer.classList.add("dt-section-container-options");
			
			for(let i = 0; i < this.queryData.options.length; i++) {
				let optionItem = new OptionItem(this.queryData, i).createOption();
				this.optionsContainer.appendChild(optionItem.dom);
				this.optionItems[i] = optionItem;
				
				/* Make option item draggable */
				this.makeOptionDraggable(optionItem);
				/* Reset option item if moved out of assigned input item upon window size chage */
				this.optionItemResetPageResizeListener(optionItem);
			}
			
			let optionsClearer = document.createElement("div");
			optionsClearer.classList.add("dt-options-clearer");
			this.optionsContainer.appendChild(optionsClearer);
			
			return this.optionsContainer;
		}
		return false;
	}
	
	/**
	 * Resets OptionItem if moved out of assigned input item upon window size chage.
	 * @param {OptionItem} optionItem - Use the passed optionItem to add an action to upon event listener call.
	 * @param {function} callback - Potential callback function.
	 * @return {QueryItem} - Returns instance of this QueryItem.
	 */
	optionItemResetPageResizeListener(optionItem, callback) {
		let inlineCSSbackup = optionItem.dom.getAttribute("style");
		window.addEventListener("resize", () => {
			if(optionItem.input) {
				if(!this.checkIfOptionItemIsInsideInput(optionItem)) {	
					/* Remove all data attributes on position reset */
					let attributesToRemove = [];
					for(let i = 0; i < optionItem.dom.attributes.length; i++) {
						if(optionItem.dom.attributes[i].name.indexOf("data-") === 0) {
							attributesToRemove.push(optionItem.dom.attributes[i].name);
						}
					}
					
					for(let i = 0; i < attributesToRemove.length; i++) {						
						optionItem.dom.removeAttribute(attributesToRemove[i]);
					}
					
					/* Position reset */				
					optionItem.dom.style = inlineCSSbackup;
					optionItem.positionChanged();
					optionItem.dom.classList.add("dt-option-reset");
				}
			}
		});
		return this;
	}
	
	/**
	 * Sets draggable plugin for this QueryItem.
	 * @param {(string|function)} [draggablePlugin] - If string passed one of the pre-set ones. If function passed thant this will be used for draggable. If ommited the default will be set.
	 */
	set draggable(draggablePlugin) {
		draggablePlugin = draggablePlugin || "default";
		
		if(typeof draggablePlugin === "function") {
			this.draggablePlugin = draggablePlugin;
			return;
		}
		
		switch(draggablePlugin) {
			case "jQuery":
				this.draggablePlugin = CommonFormFunctions.draggablePlugin_jQuery;
				break;
			case "interact":
				this.draggablePlugin = CommonFormFunctions.draggablePlugin_interact;
				break;
			default:
				this.draggablePlugin = CommonFormFunctions.draggablePlugin_interact;
		}
	}
	
	/**
	 * Get pre-set draggable plugin or default if none set.
	 * @return {function} - A function that takes care of draggable.
	 */
	get draggable() {
		return this.draggablePlugin || CommonFormFunctions.draggablePlugin_interact;
	}
	
	/**
	 * Makes passed OptionItem graggable.
	 * Requires interact.js module!
	 * @param {OptionItem} optionItem - Use the passed optionItem to make it draggable.
	 * @return {QueryItem} - Returns instance of this QueryItem.
	 */
	makeOptionDraggable(optionItem) {
		/* Adds "option-draggable" class name to an OptionItem's dom that is about to become draggable */
		optionItem.dom.classList.add("option-draggable");
		optionItem.dom.setAttribute('draggable', true);
		
		CommonFormFunctions.draggableSnippet({
			item: optionItem.dom,
			container: this.queryData.sectionContainer,
			onPosChange: () => {
				let inputItem = this.checkIfOptionItemIsInsideInput(optionItem);
				optionItem.positionChanged(inputItem);
			},
			onEnd: () => {
				let inputItem = this.checkIfOptionItemIsInsideInput(optionItem);
				optionItem.positionChanged(inputItem);
			},
			plugin: this.draggable
		});
		
		/* Draggable mobile fix: Prevent page scroll on drag event */
		optionItem.dom.addEventListener('touchmove', function(event) {
			event.preventDefault();
		}, false);
		
		return this;
	}
	
	/**
	 * Creates InputItemWrapper and assigns it to the internal list of such items.
	 * @return {QueryItem} - Returns instance of this QueryItem.
	 */
	createInput() {
		var newInput = new InputItemWrapper(this.queryData, this.inputItems.length).createInput();
		this.queryData.sectionContainer.appendChild(newInput.dom);
		this.inputWrappers.push(newInput);
		this.inputItems.push(newInput.input);
		
		return this;
	}
	
	/**
	 * Writes the content of this QueryItem to a specified DOM container.
	 * @param {dom} container - Use the passed dom element to contain this QueryItem.
	 * @return {QueryItem} - Returns instance of this QueryItem.
	 */
	write(container) {
		this.container = container || this.queryData.sectionContainer;
		
		CommonFormFunctions.empty(this.container); // Clear section container
		CommonFormFunctions.appendStyle(this.container, this.queryData.css);
		
		/* Section Title */
		if(this.queryData.title) {
			var title = document.createElement("span");
			title.classList.add("dt-section-container-title");
			title.textContent = this.queryData.title;
			this.container.appendChild(title);
		}
		
		/* Section Subtitle */
		if(this.queryData.subtitle) {
			var subtitle = document.createElement("span");
			subtitle.classList.add("dt-section-container-subtitle");
			subtitle.textContent = this.queryData.subtitle;
			this.container.appendChild(subtitle);
		}
		
		/* Section Input */
		if(this.queryData.type) {
			var hasOptions = false;
			
			if(this.queryData.before) {
				this.container.appendChild(this.queryData.before);
			}
			
			var count = 0;
			do {
				this.createInput();
				count++;
			}
			while(count < this.queryData.items.length);
			
			if(this.queryData.options.length > 0) {
				if(this.queryData.optionsOrder != "before") {
					this.container.appendChild(this.renderOptions());
				}
				else {
					this.container.insertBefore(this.renderOptions(), this.inputWrappers[0].dom);
				}
			}
			
			if(this.queryData.after) {
				this.container.appendChild(this.queryData.after);
			}
		}
		
		this.queryData.sectionContainer = this.container;
		
		return this;
	};
}


/**
 * Exception block for QueryItem
 * @see {@link https://developer.mozilla.org/cs/docs/Web/JavaScript/Reference/Statements/throw|MDN Throw Statement}
 * @param {Object} message - Exception message Object.
 * @param {string} message.message - Message text.
 */
function QueryItemException(message) {
	this.message = message;
	this.name = "QueryItemException";
}