vercengen/components/ComponentMultiTag.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`.
 * 
 * Represents a multi tag input with unique metadata attributes for keywording.
 * - Functional binding: <span color=00ffff>veMultiTag</span>().
 * 
 * ##### Constructor:
 * - `arg0_value`: {@link Array}<{@link string}>
 * - `arg1_options`: {@link Object}
 *   - `.tags_key="global"`: {@link string} - The namespace to assign to the current MultiTag component to be shared between instances.
 * 
 * ##### Instance:
 * - `.registry_array`: {@link Array}<{@link string}> - Read-only. Global registry array shared between instances.
 * - `.v`: {@link Array}<{@link string}>
 *   
 * ##### Methods:
 * - <span color=00ffff>{@link ve.MultiTag.notifyAllInstances|notifyAllInstances}</span>()
 * - <span color=00ffff>{@link ve.MultiTag.refresh|refresh}</span>()
 * - <span color=00ffff>{@link ve.MultiTag.updateLocalTags|updateLocalTags}</span>()
 * 
 * @augments ve.Component
 * @memberof ve.Component
 * @type {ve.MultiTag}
 */
ve.MultiTag = class extends ve.Component {
	static instances = {};
	
	constructor (arg0_value, arg1_options) {
		//Convert from parameters
		let value = Array.toArray(arg0_value);
		let options = (arg1_options) ? arg1_options : {};
			super(options);
		
		//Initialise options
		options.attributes = (options.attributes) ? options.attributes : {};
		
		//Declare local instance variables
		this.element = document.createElement("div");
			this.element.setAttribute("component", "ve-multi-tag");
			Object.iterate(options.attributes, (local_key, local_value) => {
				this.element.setAttribute(local_key, local_value.toString());
			});
			this.element.instance = this;
		this.options = options;
		
		this.components_obj = {};
		/**
		 * @type {string[]}
		 */
		this.local_tags = value.filter(v => v != null && v !== "");
		
		//Register the current instance for synchronisation
		let key = this.options.tags_key || "global";
		if (!ve.MultiTag.instances[key])
			ve.MultiTag.instances[key] = [];
		ve.MultiTag.instances[key].push(this);
		
		//Populate registry with initial local_tags; call this.refresh()
		for (let tag of this.local_tags)
			if (!this.registry_array.includes(tag))
				this.registry_array.push(tag);
		this.refresh();
		this.v = this.value;
	}
	
	/**
	 * Returns the global registry array shared by all {@link ve.MultiTag} components with this namespace.
	 * - Accessor of: {@link ve.MultiTag}
	 *
	 * @alias registry_array
	 * @memberof ve.Component.ve.MultiTag
	 * @type {string[]}
	 * 
	 * @returns {string[]}
	 */
	get registry_array () {
		//Declare local instance variables
		let key = this.options.tags_key || "global";
		
		//Initialise registry array first if not defined
		if (!ve.registry.settings.MultiTag[key])
			ve.registry.settings.MultiTag[key] = [];
		
		//Return statement
		return ve.registry.settings.MultiTag[key];
	}
	
	/**
	 * Returns the string array stored by the component.
	 * - Accessor of: {@link ve.MultiTag}
	 *
	 * @alias v
	 * @memberof ve.Component.ve.MultiTag
	 * @type {string[]}
	 */
	get v () {
		//Return statement
		return this.local_tags;
	}
	
	/**
	 * Sets the string array stored by the component.
	 * - Accessor of: {@link ve.MultiTag}
	 *
	 * @alias v
	 * @memberof ve.Component.ve.MultiTag
	 * 
	 * @param {string[]} arg0_value
	 */
	set v (arg0_value) {
		//Declare local instance variables
		let value = (arg0_value) ? Array.toArray(arg0_value) : arg0_value;
		
		//Refresh, set value, then fire from binding
		this.local_tags = value;
		this.refresh();
		this.fireFromBinding();
	}
	
	/**
	 * Notifies all instances sharing the same `.options.tags_key` and updates their tag suggestions.
	 * - Method of: {@link ve.MultiTag}
	 *
	 * @alias notifyAllInstances
	 * @memberof ve.Component.ve.MultiTag
	 */
	notifyAllInstances () {
		//Declare local instance variables
		let key = this.options.tags_key || "global";
		
		//Refresh all instances with this key
		let instances = ve.MultiTag.instances[key] || [];
			instances.forEach(instance => instance.refresh());
	}
	
	/**
	 * Refreshes the current {@link ve.List} display as well as any {@link ve.Datalist} components attached to it.
	 * - Method of: {@link ve.MultiTag}
	 *
	 * @alias refresh
	 * @memberof ve.Component.ve.MultiTag
	 */
	refresh () {
		//Declare local instance variables
		this.datalist_options = {};
		
		//Iterate over this.registry_array; build suggestions from registry_array
		for (let i = 0; i < this.registry_array.length; i++)
			this.datalist_options[this.registry_array[i]] = this.registry_array[i];
		
		//Create this.components_obj.datalist if ill-defined
		if (!this.components_obj.datalist) {
			this.components_obj.datalist = new ve.Datalist(this.datalist_options);
		} else {
			this.components_obj.datalist.v = this.datalist_options;
		}
		
		//Create this.components_obj.list if ill-defined
		if (!this.components_obj.list) {
			this._initialising = true;
			let element_options = {
				onuserchange: (v, e) => {
					if (this._initialising) return;
					if (this.local_tags) {
						//Count how many times v already appears in the list excluding the current input
						let duplicate_count = 0;
						
						for (let i = 0; i < this.components_obj.list.v.length; i++) {
							let item = this.components_obj.list.v[i];
							if (item.element !== e.element && item.v === v)
								duplicate_count++;
						}
						
						if (duplicate_count > 0) {
							veToast(`<icon>warning</icon> This tag is a duplicate of a previous tag, and will not be registered.`);
							e.v = "";
						} else {
							if (!this.registry_array.includes(v))
								this.registry_array.push(v);
							this.updateLocalTags();
							this.notifyAllInstances();
							this.fireToBinding();
						}
					}
				}
			};
			this.components_obj.datalist.options = element_options;
			this.components_obj.list = new ve.List([this.components_obj.datalist], {
				onuserchange: (v, e) => {
					if (this._initialising) return;
					this.updateLocalTags();
					this.notifyAllInstances();
					this.fireToBinding();
				},
				style: {
					padding: 0
				},
				options: element_options,
			});
			this.components_obj.list.bind(this.element);
			
			//Iterate over all this.local_tags; initialise list items from local_tags
			if (this.local_tags.length - 1 > 0)
				for (let i = 0; i < this.local_tags.length - 1; i++)
					this.components_obj.list.addItem();
			for (let i = 0; i < this.local_tags.length; i++)
				this.components_obj.list.v[i].v = this.local_tags[i];
			this._initialising = false;
		} else {
			//Update suggestions on all datalist items in the list
			this.components_obj.datalist.v = this.datalist_options;
			this.components_obj.list.placeholder = this.datalist_options;
			
			//Iterate over all items in the current list
			for (let i = 0; i < this.components_obj.list.v.length; i++) {
				this.components_obj.list.v[i].v = this.datalist_options;
			}
		}
	}
	
	/**
	 * Updates tags locally and ensures that {@link this.registry_array} is synchronised correctly to account for them.
	 * - Method of: {@link ve.MultiTag}
	 *
	 * @alias updateLocalTags
	 * @memberof ve.Component.ve.MultiTag	
	 */
	updateLocalTags () {
		//Declare local instance variables
		let new_array = [];
		
		//Iterate over all entries in the current list
		if (this.components_obj.list)
			for (let i = 0; i < this.components_obj.list.v.length; i++) {
				let local_value = this.components_obj.list.v[i].v;
				if (local_value && local_value.length > 0)
					new_array.push(local_value);
			}
		
		//Refresh this.local_tags
		this.local_tags = new_array;
		
		//Add new tags to registry
		for (let tag of new_array)
			if (!this.registry_array.includes(tag))
				this.registry_array.push(tag);
	}
};

//Functional binding

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