js/vercengen/features/ContextMenu.js

/**
 * Refer to <span color = "yellow">{@link ve.Feature}</span> for methods or fields inherited from the parent, such as automatic destructuring.
 * 
 * Represents a ContextMenu Feature that contains a set of components. {@link ve.Window} instances are stored in `.windows`. Recursive.
 * - Functional binding: <span color=00ffff>veContextMenu</span>().
 * - Inherits feature: {@link ve.Window}
 * 
 * The immediate {@link ve.Button} element bound to the ContextMenu instance is contained in `.element`.
 * 
 * ##### Constructor:
 * - `arg0_components_obj`: {@link Object}<{@link ve.Component}>
 * - `arg1_options`: {@link Object}
 *   - `.anchor="left"/"right"` - Either 'left'/'right'. Position defaults to the side that has the most space.
 *   - `.button_options`: {@link ve.Button|ve.Button.options}
 *   - `.id`: {@link string}
 *   - `.mode="static_ui"` - Either 'static_ui'/'static_window'/'window'.
 *   - `.x=HTML.mouse_x`: {@link number}
 *   - `.y=HTML.mouse_y`: {@link number}
 *   
 * ##### Methods:
 * - <span color=00ffff>{@link ve.ContextMenu.addContextMenu|addContextMenu}</span>(arg0_components_obj: {@link Object}<{@link ve.Component}>, arg1_options: {@link ve.Window|ve.Window.options}) | {@link ve.ContextMenu}
 *   - `arg1_options`: {@link ve.Window|ve.Window.options}
 *     - `.id`: {@link string} - The ID to prevent duplicate context menus from being opened.
 * - <span color=00ffff>{@link ve.ContextMenu.close|close}</span>() | this:{@link ve.ContextMenu}
 * - <span color=00ffff>{@link ve.ContextMenu.getCurrentOffset|getCurrentOffset}</span>() | {@link number}
 * - <span color=00ffff>{@link ve.ContextMenu.open|open}</span>() | this:{@link ve.ContextMenu}
 * - <span color=00ffff>{@link ve.ContextMenu.removeContextMenu|removeContextMenu}</span>(arg0_index: this:{@link number}) | {@link ve.ContextMenu}
 * 
 * @augments ve.Feature
 * @augments {@link ve.Feature}
 * @memberof ve.Feature
 * @type {ve.ContextMenu}
 */
ve.ContextMenu = class extends ve.Feature { //[WIP] - Finish class body
	constructor (arg0_components_obj, arg1_options) {
		//Convert from parameters
		let components_obj = arg0_components_obj;
		let options = (arg1_options) ? arg1_options : {};
			super(components_obj, options);
		
		//Initialise options
		options.anchor = (options.anchor) ? options.anchor :
			(HTML.mouse_x < window.innerHeight/2) ? "left" : "right";
		options.button_options = (options.button_options) ? options.button_options : {};
		options.mode = (options.mode) ? options.mode : "static_ui";
		options.x = (options.x !== undefined) ? options.x : HTML.mouse_x;
		options.y = (options.y !== undefined) ? options.y : HTML.mouse_y;
		
		//Declare local instance variables
		this.button = new ve.Button((e) => {
			this.close();
			this.open();
		}, options.button_options);
		this.components_obj = components_obj;
		this.element = this.button.element;
		this.options = options;
		this.windows = [];
		
		this.addContextMenu(this.components_obj, { 
			id: options.id
		});
	}
	
	/**
	 * Adds a given context menu with a set `.options.id` to prevent duplicates.
	 * - Method of: {@link ve.ContextMenu}
	 * 
	 * @param {{"<component_key>": ve.Component}} arg0_components_obj
	 * @param {Object} [arg1_options]
	 *  @param {string} [arg1_options.anchor] - Either 'left'/'right', depending on available screen-space.
	 *  @param {string} [arg1_options.id]
	 *  
	 * @returns {ve.Window}
	 */
	addContextMenu (arg0_components_obj, arg1_options) {
		//Convert from parameters
		let components_obj = arg0_components_obj;
		let options = (arg1_options) ? arg1_options : {};
		
		//Initialise options
		if (options.id !== undefined) {
			//Iterate over all extant this.windows; internal guard clause if ID already exists
			let all_window_els = document.querySelectorAll("div.ve.window");
			let break_function = false;
			
			all_window_els.forEach((local_el) => {
				if (local_el.context_menu_instance)
					if (local_el.context_menu_instance.options.id === options.id)
						break_function = true;
			});
			this.windows.forEach((local_window) => {
				if (local_window.id === options.id)
					break_function = true;
			});
			
			if (break_function) return;
		} else {
			console.warn(`ve.ContextMenu: addContextMenu - options.id is not defined! options.id should be defined to prevent duplicate context menus from opening up.`);
		}
		
		//Declare local instance variables
		let actual_x;
			//Populate actual_x
			if (this.windows.length > 0) {
				actual_x = this.windows[0].element.offsetLeft;
			} else {
				actual_x = this.options.x;
			}
		let actual_y;
			//Populate actual_y
			if (this.windows.length > 0) {
				actual_y = this.windows[0].element.offsetTop;
			} else {
				actual_y = this.options.y;
			}
		let current_x_offset = this.getCurrentOffset();
			
		//Modify actual_x, actual_y
		if (this.options.anchor === "left") {
			actual_x += current_x_offset;
		} else if (this.options.anchor === "right") {
			actual_x -= current_x_offset;
		}
		
		//Create new ve.Window to represent the current context menu
		let window_obj = new ve.Window(components_obj, {
			mode: this.options.mode,
			x: actual_x,
			y: actual_y,
			...options
		});
		let close_window_button = document.createElement("div");
			close_window_button.classList.add("context-menu-close-button");
			close_window_button.innerHTML = `<icon>close</icon>`;
			close_window_button.onclick = (e) => {
				//Close preceding context menus
				let local_index = parseInt(close_window_button.getAttribute("data-index"));
				this.removeContextMenu(local_index);
			};
			close_window_button.setAttribute("data-index", this.windows.length.toString());
			
			window_obj.element.appendChild(close_window_button);
			window_obj.element.context_menu_instance = this;
			if (options.id)
				window_obj.id = options.id;
		
		this.windows.push(window_obj);
		
		//Post-processing
		//Right-align handling
		if (this.windows.length === 1 && this.options.anchor === "right") {
			actual_x -= this.windows[0].element.offsetWidth;
			setTimeout(() => {
				this.windows[0].element.style.left = `${actual_x}px`;
			}, 100);
		}
		
		//Return statement
		return window_obj;
	}
	
	/**
	 * Closes the present context menu and all its associated layers.
	 * - Method of: {@link ve.ContextMenu}
	 * 
	 * @returns {ve.ContextMenu}
	 */
	close () {
		//Iterate over all open context menus to close them in this.windows
		for (let i = 0; i < this.windows.length; i++)
			this.windows[i].remove();
		this.windows = [];
		
		//Return statement
		return this;
	}
	
	/**
	 * Returns the current X offset in px from the anchored first layer. Default padding is 4px.
	 * - Method of: {@link ve.ContextMenu}
	 * 
	 * @returns {number}
	 */
	getCurrentOffset () {
		//Declare local instance variables
		let offset_x = 0;
		
		//Iterate over all current windows in the context menu 
		for (let i = 0; i < this.windows.length; i++)
			offset_x += this.windows[i].element.offsetWidth + 4;
		
		//Return statement
		return offset_x;
	}
	
	/**
	 * Opens the present context menu with its bound {@link this.components_obj}.
	 * - Method of: {@link ve.ContextMenu}
	 * 
	 * @returns {ve.ContextMenu}
	 */
	open () {
		//Open current context menu with bound this.components_obj
		this.addContextMenu(this.components_obj);
		
		//Return statement
		return this;
	}
	
	/**
	 * Removes a context menu layer based on its order.
	 * - Method of: {@link ve.ContextMenu} 
	 * 
	 * @param {number} arg0_index
	 * 
	 * @returns {ve.ContextMenu}
	 */
	removeContextMenu (arg0_index) {
		//Convert from parameters
		let index = parseInt(arg0_index);
		
		//Attempt to remove all the context menus from ve.Windows after the current index
		try {
			for (let i = this.windows.length - 1; i >= index; i--) {
				this.windows[i].remove();
				this.windows.splice(i, 1);
			}
		} catch (e) {
			console.error(`ve.ContextMenu: The present index ${index} does not exist in this.windows:`, this.windows);
		}
		
		//Return statement
		return this;
	}
};

//Functional binding

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