vercengen/components/ComponentSearchSelect.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`.
 * 
 * Search select/filter component used as a raw interface pool with a searchbar in which users can look up matching items with selected 'data-' attributes. Elements with the attribute `ve-display=true` will always be shown.
 * - Functional binding: <span color=00ffff>veSearchSelect</span>().
 * 
 * ##### Constructor:
 * - `arg0_components_obj`: {@link Object}<{@link ve.Component}> - The individual items to append to the current search select field.
 * - `arg1_options`: {@link Object}
 *   - `.display="inline"`: {@link string}
 *   - `.header_components_obj`: {@link Object}<{@link ve.Component}>
 *   - `.hide_filter=false`: {@link boolean} - Whether to hide the filter tool.
 *   - `.filter_names`: {@link function}(arg0_attribute_key:{@link string})|{@link Object}
 *     - `<attribute_key>`: {@link string}
 *   - `.search_select_els`: {@link function} | {@link Array}<{@link HTMLElement}> - The function that returns the search select elements in question, usually `document.querySelectorAll`.  
 *   - `.search_keys=["name"]`: {@link Array}<{@link string}>
 *     
 * ##### Instance:
 * - `.components_obj`: {@link Object}<{@link ve.Component}>
 * - `.filters`: {@link Object} - Any filters currently applied to the component.
 *   - `<filter_key>`: true
 * - `.search_value`: {@link string} - The current user search query.
 * - `.v`: {@link this.components_obj} - Accessor. The current components_obj mounted to ve.SearchSelect.
 * 
 * ##### Methods:
 * - <span color=00ffff>{@link ve.SearchSelect.getComponentElements|getComponentElements}</span>() | {@link Array}<{@link HTMLElement}>
 * - <span color=00ffff>{@link ve.SearchSelect.updateSearchFilter|updateSearchFilter}</span>()
 * 
 * @augments ve.Component
 * @memberof ve.Component
 * @type {ve.SearchSelect}
 */
ve.SearchSelect = class extends ve.Component {
	static reserved_keys = ["element", "id", "name"];
	
	constructor (arg0_components_obj, arg1_options) {
		//Convert from parameters
		let components_obj = (arg0_components_obj) ? arg0_components_obj : {};
		let options = (arg1_options) ? arg1_options : {};
			super(options);
			
		//Initialise options
		options.attributes = (options.attributes) ? options.attributes : {};
		options.search_keys = (options.search_keys) ? options.search_keys : ["name"];
		options.style = {
			padding: 0,
			...options.style
		};
		
		//Declare local instance variables
		this.element = document.createElement("div");
			this.element.setAttribute("component", "ve-search-select");
			this.element.instance = this;
			HTML.setAttributesObject(this.element, options.attributes);
		this.filters = {};
		this.options = options;
		this.search_value = "";
		
		//Append components_obj to this.element
		this.v = components_obj;
	}
	
	/**
	 * Returns the current {@link this.components_obj}.
	 * - Accessor of: {@link ve.SearchSelect}
	 * 
	 * @alias v
	 * @memberof ve.Component.ve.SearchSelect
	 * @type {ve.Component[]}
	 */
	get v () {
		//Return statement
		return this.components_obj;
	}
	
	/**
	 * Sets the current {@link this.components_obj} displayed in the search select box.
	 * - Accessor of: {@link ve.Hierarchy}
	 * 
	 * @alias v
	 * @memberof ve.Component.ve.SearchSelect
	 * @param arg0_components_obj {ve.Component[]}
	 */
	set v (arg0_components_obj) {
		//Convert from parameters
		let components_obj = arg0_components_obj;
		
		//Reset element; re-append all components in components_obj to element as though it were a ve.RawInterface
		this.components_obj = components_obj;
		this.element.innerHTML = "";
		
		let searchbar_interface = new ve.RawInterface({
			searchbar_icon: new ve.HTML("<icon>search</icon>", { style: { padding: `var(--cell-padding)` } }),
			searchbar_input: new ve.Datalist({}, {
				attributes: {
					placeholder: loc("ve.registry.localisation.Hierarchy_search_for_item")
				},
				name: " ",
				onuserchange: (v) => {
					this.search_value = v;
					this.updateSearchFilter();
					this.fireToBinding();
				}
			}),
			searchbar_filter: new ve.Button(() => {
				//Declare local instance variables
				let all_unique_attributes = [];
				
				//Add checkbox context menu based on data- attributes
				let all_component_els = this.getComponentElements();
				
				//Iterate over all_component_els
				for (let i = 0; i < all_component_els.length; i++)
					//Iterate over all local_value.element.attributes
					for (let x = 0; x < all_component_els[i].attributes.length; x++) {
						let local_attribute = all_component_els[i].attributes[x].nodeName;
						
						if (local_attribute.startsWith("data-"))
							if (!all_unique_attributes.includes(local_attribute))
								all_unique_attributes.push(local_attribute);
					}
				all_unique_attributes.sort();
				
				//Iterate over all_unique_attributes, add to filter context menu
				let checkbox_components_obj = {};
				
				for (let i = 0; i < all_unique_attributes.length; i++) {
					let local_name = all_unique_attributes[i];
						if (this.options?.filter_names)
							if (typeof this.options.filter_names === "function") { 
								local_name = this.options.filter_names(local_name);
							} else {
								local_name = this.options.filter_names[all_unique_attributes[i]];
							}
					
					checkbox_components_obj[all_unique_attributes[i]] = new ve.Checkbox(this.filters[all_unique_attributes[i]], {
						name: local_name,
						onuserchange: (v) => {
							if (v === true) {
								this.filters[all_unique_attributes[i]] = true;
							} else {
								delete this.filters[all_unique_attributes[i]];
							}
							this.updateSearchFilter();
							this.fireToBinding();
						}
					});
				}
					
				if (all_unique_attributes.length === 0)
					checkbox_components_obj.no_elements_found = new ve.HTML(loc("ve.registry.localisation.SearchSelect_no_elements_found"));
				
				//Open new context menu
				let local_context_menu = new ve.ContextMenu({
					filter_header: new ve.HTML(`<b>${loc("ve.registry.localisation.SearchSelect_search_filter")}</b><br><br>`, { x: 0, y: 0 }),
					...checkbox_components_obj
				}, {
					id: "search_select_filter"
				})
			}, { 
				name: "<icon>filter_alt</icon>",
				tooltip: loc("ve.registry.localisation.SearchSelect_search_filter"),
				style: {
					display: (this.options.hide_filter) ? "none" : "block",
					marginLeft: "auto"
				}
			}),
			...this.options.header_components_obj
		}, {
			name: " ",
			attributes: {
				"ve-searchbar": "true"
			},
			style: this.options.searchbar_style
		});
		searchbar_interface.bind(this.element);
		
		//Populate this.element from this.components_obj
		Object.iterate(this.components_obj, (local_key, local_value) =>
			this.element.appendChild(local_value.element));
		this.fireFromBinding();
	}
	
	/**
	 * Returns all actual component elements.
	 * - Method of: {@link ve.SearchSelect}
	 * 
	 * @alias getComponentElements
	 * @memberof ve.Component.ve.SearchSelect
	 * 
	 * @returns {HTMLElement[]}
	 */
	getComponentElements () {
		//Internal guard clause if this.options.search_select_els is defined
		if (this.options.search_select_els) return this.options.search_select_els();
		
		//Declare local instance variables
		let all_search_select_els = [];
			Object.iterate(this.components_obj, (local_key, local_value) =>
				all_search_select_els.push(local_value.element));
		
		//Return statement
		return all_search_select_els;
	}
	
	/**
	 * Updates the present search filter to be inline with {@link this.filters} and {@link this.search_value}.
	 * - Method of: {@link ve.SearchSelect}
	 * 
	 * @alias updateSearchFilter
	 * @memberof ve.Component.ve.SearchSelect
	 */
	updateSearchFilter () {
		//Declare local instance variables
		let all_search_select_els = this.getComponentElements();
		
		//If name and filters are nothing, restore visibility to all hidden results
		if (this.search_value.length === 0 && Object.keys(this.filters).length === 0) {
			for (let i = 0; i < all_search_select_els.length; i++)
				all_search_select_els[i].style.display = (this.options.display) ? this.options.display : "inherit";
		} else {
			//Iterate over all search_select_els
			for (let i = 0; i < all_search_select_els.length; i++) {
				let has_valid_substring = false;
				let show_element = false;
				
				//Iterate over all this.options.search_keys to assess has_valid_substring
				for (let x = 0; x < this.options.search_keys.length; x++)
					if (all_search_select_els[i].instance) try {
						if (all_search_select_els[i].instance[this.options.search_keys[x]].toLowerCase().trim().indexOf(this.search_value.toLowerCase().trim()) !== -1) {
							has_valid_substring = true;
							break;
						}
					} catch (e) {
						console.error(`Could not find ${this.options.search_keys[x]}, Instance:`, all_search_select_els[i].instance, e);
					}
				
				//If it has a valid substring, look for valid attributes
				if (has_valid_substring) {
					//Check if element has data- attribute in filters
					let has_valid_attribute = false;
					
					if (Object.keys(this.filters).length === 0) {
						has_valid_attribute = true;
					} else {
						Object.iterate(this.filters, (local_key, local_value) => {
							if (all_search_select_els[i].getAttribute(local_key))
								has_valid_attribute = true;
						});
					}
					
					if (has_valid_attribute)
						show_element = true;
				}
				if (all_search_select_els[i].getAttribute("ve-display"))
					show_element = true;
				
				if (show_element || all_search_select_els[i]?.instance?.options?.disabled === true) {
					all_search_select_els[i].style.display = (this.options.display) ? this.options.display : "inline";
				} else {
					all_search_select_els[i].style.display = "none";
				}
			}
		}
	}
};

//Functional binding

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