js/vercengen/features/Window.js

/**
 * Refer to <span color = "yellow">{@link ve.Feature}</span> for methods or fields inherited from the parent, such as automatic destructuring.
 * 
 * Represents a Window Feature that contains a set of components which are wrapped inside an Interface.
 * - Inherited by: {@link ve.ContextMenu}, {@link ve.Modal}, {@link ve.PageMenuWindow}
 *
 * ##### Constructor:
 * - `arg0_components_obj`: {@link Object}<{@link ve.Component}>
 * - `arg1_options`: {@link Object}
 *   - `anchor="top_left"` - Either 'bottom_left'/'bottom_right'/'top_left'/'top_right'.
 *   - `height="auto"`: {@link number}
 *   - `width="12rem"`: {@link number}
 *   - `x=HTML.mouse_x`: {@link number}
 *   - `y=HTML.mouse_y`: {@link number}
 *   -
 *   - `do_not_wrap=false`: {@link boolean} - Whether to disable wrapping in an always open ve.Interface.
 *   - `mode="window"`: {@link string} - Either 'static_ui'/'static_window'/'window'.
 *   - `name=""`: {@link string} - Auto-resolves to 'Window' instead if `.can_rename=true`.
 *   - `theme`: {@link string} - The CSS theme to apply to the Feature.
 *   -
 *   - `can_close`: {@link boolean}
 *   - `can_rename`: {@link boolean}
 *   - `draggable`: {@link boolean}
 *   - `headless`: {@link boolean}
 *   - `resizeable`: {@link boolean}
 *   
 * ##### Instance:
 * - `.v`: {@link Object}<{@link ve.Component}>
 *
 * ##### Methods:
 * - <span color=00ffff>{@link ve.Window.getZIndex|getZIndex}</span>() | {@link number}
 * - <span color=00ffff>{@link ve.Window.select|select}</span>()
 * - <span color=00ffff>{@link ve.Window.setCoords|setCoords}</span>(arg0_x:{@link number}, arg1_y:{@link number})
 * - <span color=00ffff>{@link ve.Window.setSize|setSize}</span>(arg0_width:{@link number}|{@link string}, arg1_height:{@link number}|{@link string})
 * - <span color=00ffff>{@link ve.Window.refresh|refresh}</span>(arg0_components_obj:{@link Object}<{@link ve.Component}>)
 * 
 * ##### Static Fields:
 * - `.instances`: {@link Array}<{@link ve.Window}>
 * 
 * ##### Static Methods:
 * - <span color=00ffff>{@link ve.Window.getHighestZIndex}</span>(arg0_options:{ return_object=false: {@link boolean} }) | {@link number}|{@link ve.Window}
 *   
 * @augments ve.Feature
 * @augments {@link ve.Feature}
 * @memberof ve.Feature
 * @type {ve.Window}
 */
ve.Window = class extends ve.Feature {
	//Declare local static variables
	static instances = [];
	
	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
		let is_coords_well_defined = (typeof options.x === "number" && typeof options.y === "number");
		if (options.mode === undefined)
			options.mode = (is_coords_well_defined) ? "static_ui" : "window";
		
		options.anchor = (options.anchor) ? options.anchor : "top_left";
		options.height = (options.height !== undefined) ? options.height : "auto";
		options.width = (options.width !== undefined) ? options.width : "12rem";
		options.x = (options.x !== undefined) ? options.x : HTML.mouse_x;
		options.y = (options.y !== undefined) ? options.y : HTML.mouse_y;
		
		//Parse options
		let mode_behaviour = {
			static_ui: {
				can_close: false,
				can_rename: false,
				draggable: false,
				resizeable: false,
				headless: true,
				is_static: true
			},
			static_window: {
				can_close: false,
				can_rename: false,
				draggable: false,
				resizeable: false,
				headless: false,
				is_static: true
			},
			window: {
				can_close: true,
				can_rename: true,
				draggable: true,
				resizeable: true,
				headless: false,
				is_static: false
			}
		};
		options = {
			...mode_behaviour[options.mode],
			...options
		};
		
		options.name = (options.name) ? options.name : "";
			if (options.can_rename && options.name === "") options.name = `Window`;
			this.name = options.name;
			
		//Declare local instance variables
		this.options = options;
		
		//Parse to ve.HTML if components_obj is typeof function or string
		//this.components_obj
		if (typeof components_obj === "function" || typeof components_obj === "string") {
			this.components_obj = {
				html: new ve.HTML(components_obj)
			};
		} else if (components_obj.is_vercengen_component) {
			this.components_obj = {
				component: components_obj
			};
		} else {
			this.components_obj = components_obj;
		}
		this.element = document.createElement("div");
		
		//Iterate over all .attributes if extant to set them
		if (typeof options.attributes === "object")
			Object.iterate(options.attributes, (local_key, local_value) => {
				if (local_key === "class") {
					this.element.classList.add(...local_value.toString().split(" "));
				} else {
					this.element.setAttribute(local_key, local_value.toString());
				}
			});
		
		this.id = Class.generateRandomID(ve.Window);
		this.x = options.x;
		this.y = options.y;
		
		//Adjust height/width, position
		{
			setTimeout(() => {
				this.setCoords(this.x, this.y);
				this.setSize(options.width, options.height);
			});
		}
		
		//Populate Window element
		this.element.instance = this;
		this.element.classList.add("class", `ve`, `window`);
		this.element.id = this.id;
		this.element.innerHTML = `
			${(!options.headless) ? `<div id = "feature-header" class = "feature-header">
				<span id = "window-name"${(this.options.can_rename) ? ` contenteditable = "plaintext-only"` : ""}>${this.name}</span>
			</div>` : ""}
			<div id = "feature-body" class = "feature-body"></div>
		`;
		this.element.style.zIndex = ve.Window.instances.length.toString();
		
		//Instantiate element handlers
		if (this.options.can_close && !this.options.headless) {
			let close_button = document.createElement("img");
				close_button.id = "close-button";
				close_button.src = `./UF/gfx/close_icon_dark.png`;
			this.element.querySelector(`#feature-header`).appendChild(close_button);
			
			close_button.onclick = () => {
				this.remove();
			};
		}
		if (!this.options.headless)
			HTML.createSection({
				selector: `.ve.window[id="${this.id}"] #feature-header, .ve.window[id="${this.id}"] #feature-body`
			});
		if (this.options.draggable) {
			this.element.classList.add("draggable");
			HTML.elementDragHandler(this.element, {
				is_resizable: (this.options.resizeable)
			});
		}
		
		//Push Window instance to ve.Window.instances
		this.refresh(this.components_obj);
		ve.window_overlay_el.appendChild(this.element);
		ve.Window.instances.push(this);
	}
	
	/**
	 * Returns the components currently visible in the Window.
	 * - Accessor of: {@link ve.Window}
	 * 
	 * @returns {{"<component_key>": ve.Component}}
	 */
	get v () {
		//Return statement
		return this.components_obj;
	}
	
	/**
	 * Sets the components visible in the current window.
	 * - Accessor of: {@link ve.Window}
	 * 
	 * @param {{"<component_key>": ve.Component}} arg0_components_obj
	 */
	set v (arg0_components_obj) {
		//Convert from parameters
		let components_obj = arg0_components_obj;
		
		//Refresh local instance
		this.refresh(components_obj);
	}
	
	/**
	 * Returns the current z-index of this {@link ve.Window}.
	 * - Method of: {@link ve.Window}
	 *
	 * @returns {number}
	 */
	getZIndex () {
		//Return statement
		return parseInt(getComputedStyle(this.element)["z-index"]);
	}
	
	/**
	 * Refreshes the components display for the current Window.
	 * - Method of: {@link ve.Window}
	 * 
	 * @param {{"<component_key>": ve.Component}} arg0_components_obj
	 */
	refresh (arg0_components_obj) { //[WIP] - Finish ve.Interface wrapping
		//Convert from parameters
		this.components_obj = arg0_components_obj;
		
		//Declare local instance variables
		let feature_body_el = this.element.querySelector(`#feature-body`);
		
		//Automatically wrap this.components_obj in a ve.Interface object (.is_folder=false)
		if (!this.options.do_not_wrap) {
			feature_body_el.innerHTML = "";
			this._interface = new ve.Interface(this.components_obj, { //[WIP] - Make generic via .is_container field for ve.Interface
				is_folder: false,
				name: " ",
				
				style: {
					padding: 0
				}
			});
			this._interface.element.setAttribute("class", "ve-disable-nesting");
			feature_body_el.appendChild(this._interface.element);
		} else {
			delete this._interface;
			
			//Append all components in components_obj to this.element.querySelector("#feature-body")
			feature_body_el.innerHTML = "";
			Object.iterate(this.components_obj, (local_key, local_value) => {
				if (local_value.element) {
					local_value.element.id = local_key;
					feature_body_el.appendChild(local_value.element);
				}
				if (local_value.name === undefined || local_value.name === "")
					local_value.name = local_key;
			});
		}
	}
	
	/**
	 * Selects the current {@link ve.Window} instance, raising its z-index above all other Windows.
	 * - Method of: {@link ve.Window}
	 */
	select () {
		//Declare local instance variables
		let current_highest_z_index = ve.Window.getHighestZIndex() + 1;
		
		//Swap z-indices
		this.element.style.zIndex = current_highest_z_index.toString();
		ve.Window.normaliseZIndexes();
	}
	
	/**
	 * Sets the present coords of the window to a given X, Y coordinate relative to the present anchor.
	 * - Method of: {@link ve.Window}
	 * 
	 * @param {number} arg0_x
	 * @param {number} arg1_y
	 */
	setCoords (arg0_x, arg1_y) {
		//Convert from parameters
		let x = parseInt(arg0_x);
		let y = parseInt(arg1_y);
		
		//Set element X, Y position
		this.element.style.position = "absolute";
		this.element.style.bottom = "";
		this.element.style.left = "";
		this.element.style.right = "";
		this.element.style.top = "";
		HTML.applyTelestyle(this.element, {
			...HTML.getCSSPosition(this.options.anchor, x, y)
		});
	}
	
	/**
	 * Sets the given size of the current window using either numbers or CSS calculated strings in Telestyle. Any functions must return either a number/string.
	 * - Method of: {@link ve.Window}
	 * 
	 * @param {function|number|string} arg0_width
	 * @param {function|number|string} arg1_height
	 */
	setSize (arg0_width, arg1_height) {
		//Convert from parameters
		let width = arg0_width;
		let height = arg1_height;
		
		//Apply style
		HTML.applyTelestyle(this.element, {
			...HTML.getCSSSize(width, height)
		});
	}
	
	/**
	 * Returns the highest z-index over the set of all Windows in {@link ve.Window.instances}.
	 * - Static method of: {@link ve.Window}
	 * 
	 * @param {Object} [arg0_options]
	 *  @param {boolean} [arg0_options.return_object=false] - Whether to return a ve.Window instance.
	 *
	 * @returns {number|ve.Window}
	 */
	static getHighestZIndex (arg0_options) {
		//Convert from parameters
		let options= (arg0_options) ? arg0_options : {};
		
		//Declare local instance variables
		let highest_z_index = [-Infinity, undefined];
		
		//Iterate over all ve.Window.instances
		for (let i = 0; i < ve.Window.instances.length; i++) {
			let local_instance = ve.Window.instances[i];
			
			if (local_instance.getZIndex() > highest_z_index[0])
				highest_z_index = [local_instance.getZIndex(), local_instance];
		}
		
		//Return statement
		return (!options.return_object) ? highest_z_index[0] : highest_z_index[1];
	}
};

//Functional binding

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