js/vercengen/components/ComponentPageMenu.js

/**
 * Refer to <span color = "yellow">{@link ve.Component}</span> for methods or fields inherited from this Component's parent such as `.options.attributes` or `.element`.
 * 
 * Multipage menu with a relative topbar for the user to switch between tabs. Contains an interface otherwise.
 * - Functional binding: <span color=00ffff>vePageMenu</span>().
 * 
 * ##### Constructor:
 * - `arg0_page_obj`: {@link Object}
 *   - `<page_key>`: {@link Object}
 *     - `.name`: {@link string}
 *     - `.components_obj`: {@link Object}<{@link ve.Component}>
 * - `arg1_options`: {@link Object}
 *   - `.starting_page=Object.keys(page_obj)[0]`
 * 
 * ##### Instance:
 * - `.interface_el`: {@link HTMLElement}
 * - `.interfaces_obj`: {@link Object}<{@link ve.Interface}> - Contains all interfaces in all pages.
 * - `.navbar_el`: {@link HTMLElement}
 * - `.underline_el`: {@link HTMLElement}
 * - `.v`: {@link string} - The current `<page_key>` selected by the ve.PageMenu.
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.PageMenu.updateUnderline|updateUnderline}</span>()
 * 
 * @augments ve.Component
 * @augments {@link ve.Component}
 * @memberof ve.Component
 * @type {ve.PageMenu}
 */
ve.PageMenu = class extends ve.Component { //[WIP] - This should be updated later to allow arg0_page_obj to take in Array<components_obj> instead, which would create a conventional paginated menu instead of a tabbed one.
	static demo_value = {
		page_one: {
			name: "Home",
			components_obj: {}
		},
		page_two: {
			name: "About",
			components_obj: {}
		}
	};
	
	constructor (arg0_page_obj, arg1_options) {
		//Convert from parameters
		let page_obj = arg0_page_obj;
		let options = (arg1_options) ? arg1_options : {};
			super(options);
			
		//Initialise options
		options.attributes = (options.attributes) ? options.attributes : {};
		options.starting_page = (options.starting_page) ? options.starting_page : Object.keys(page_obj)[0];
		
		//Declare local instance variables
		this.element = document.createElement("div");
			this.element.setAttribute("component", "ve-page-menu");
			Object.iterate(options.attributes, (local_key, local_value) => {
				this.element.setAttribute(local_key, local_value.toString());
			});
			this.element.instance = this;
			HTML.applyTelestyle(this.element, options.style);
		this.interfaces_obj = {};
		this.options = options;
		this.navbar_el = document.createElement("nav");
			this.navbar_el.classList.add("navbar");
		
		//1. Navbar handling
		{
			//Append header element; navbar_el
			//Iterate over all keys in page_obj and create ve.Interface; tab instances for them
			Object.iterate(page_obj, (local_key, local_value) => {
				let local_name = (local_value.name) ? local_value.name : local_key;
				let local_name_el = document.createElement("div");
				local_name_el.classList.add("tab");
				if (local_key === options.starting_page)
					local_name_el.classList.add("active");
				local_name_el.id = local_key;
				local_name_el.innerHTML = local_name;
				
				//Format navbar_el; populate this.interfaces_obj
				this.navbar_el.appendChild(local_name_el);
				this.interfaces_obj[local_key] = new ve.Interface(local_value.components_obj, local_value.options);
			});
			this.underline_el = document.createElement("span");
			this.underline_el.classList.add("underline");
			this.navbar_el.appendChild(this.underline_el);
			
			//Append body element; interface_el
			this.interface_el = document.createElement("div");
			this.interface_el.id = "component-body";
			
			this.element.append(this.navbar_el, this.interface_el);
			
			//Add tab onclick handlers for this.navbar_el
			let all_tabs = this.navbar_el.querySelectorAll(`.tab`);
			
			all_tabs.forEach((local_tab) => {
				local_tab.addEventListener("click", () => {
					all_tabs.forEach((local_tab) => local_tab.classList.remove("active"));
					local_tab.classList.add("active");
					this.updateUnderline();
					this.fireToBinding();
				});
			});
		}
		
		//2. Body handling; display starting interface
		{
			this.v = options.starting_page;
			if (options.name) this.name = options.name;
		}
	}
	
	/**
	 * Returns the current page visible in the component.
	 * - Accessor of: {@link ve.Interface}
	 * 
	 * @returns {string}
	 */
	get v () {
		//Return statement
		return this.navbar_el.querySelector(`.tab.active`).id;
	}
	
	/**
	 * Sets the new page visible by the component.
	 * - Accessor of: {@link ve.Interface}
	 * 
	 * @param {string} arg0_page_key
	 */
	set v (arg0_page_key) {
		//Convert from parameters
		let page_key = arg0_page_key;
		
		//Declare local instance variables
		let active_tab_el = this.navbar_el.querySelector(`.tab[id="${page_key}"]`);
		let all_tabs = this.navbar_el.querySelectorAll(`.tab`);
		
		//Modify active tab
		all_tabs.forEach((local_tab) => local_tab.classList.remove("active"));
		if (active_tab_el) {
			active_tab_el.classList.add("active");
		} else {
			console.error(`active_tab_el could not be found for ${page_key}.`);
		}
		
		//Switch interface to selected page
		this.interface_el.innerHTML = "";
		this.interface_el.appendChild(this.interfaces_obj[page_key].element);
		setTimeout(() => {
			this.updateUnderline();
		}, 100);
		this.fireFromBinding();
	}
	
	/**
	 * Updates the underline underneath the active tab and animates it.
	 * - Method of: {@link ve.PageMenu}
	 */
	updateUnderline () {
		//Declare local instance variables
		let active_tab = this.navbar_el.querySelector(`.tab.active`);
			if (!active_tab) return;
		let underline_computed_style = window.getComputedStyle(this.underline_el);
		
		let offset_left = active_tab.offsetLeft;
		let tab_width = active_tab.offsetWidth;
		let underline_y = active_tab.offsetTop + active_tab.offsetHeight - parseFloat(underline_computed_style.height);
		
		//Snap vertically, animate horizontally
		this.underline_el.style.transition = "none";
		this.underline_el.style.top = `${underline_y}px`;
		
		requestAnimationFrame(() => {
			this.underline_el.style.left = `${offset_left}px`;
			this.underline_el.style.transition = `left 0.5s ease, width 0.5s ease`;
			this.underline_el.style.width = `${tab_width}px`;
		});
	}
};

//Functional binding

/**
 * @returns {ve.PageMenu}
 */
vePageMenu = function () {
	//Return statement
	return new ve.PageMenu(...arguments);
};