Source: asset-taglib/src/main/resources/META-INF/resources/asset_tags_selector/AssetTagsSelector.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import 'clay-multi-select';
import {ItemSelectorDialog} from 'frontend-js-web';
import Component from 'metal-component';
import Soy from 'metal-soy';
import {Config} from 'metal-state';

import templates from './AssetTagsSelector.soy';

/**
 * Wraps Clay's existing <code>MultiSelect</code> component that offers the user
 * a tag selection input.
 */
class AssetTagsSelector extends Component {
	/**
	 * @inheritDoc
	 */
	attached(...args) {
		super.attached(...args);

		this._dataSource = this._handleQuery.bind(this);
	}

	/**
	 * Opens the tag selection dialog.
	 *
	 * @param {!Event} event The event.
	 * @private
	 */
	_handleButtonClicked() {
		const sub = (str, obj) => str.replace(/\{([^}]+)\}/g, (_, m) => obj[m]);

		const uri = sub(decodeURIComponent(this.portletURL), {
			selectedTagNames: this._getTagNames()
		});

		const itemSelectorDialog = new ItemSelectorDialog({
			buttonAddLabel: Liferay.Language.get('done'),
			eventName: this.eventName,
			title: Liferay.Language.get('tags'),
			url: uri
		});

		itemSelectorDialog.open();

		itemSelectorDialog.on('selectedItemChange', event => {
			const selectedItems = event.selectedItem;

			if (selectedItems) {
				const newValues =
					selectedItems.items.length > 0
						? selectedItems.items.split(',')
						: [];
				const oldItems = this.selectedItems.slice();
				const oldValues = oldItems.map(item => item.value);
				const valueMapper = item => {
					return {
						label: item,
						value: item
					};
				};

				const addedItems = newValues
					.filter(value => !oldValues.includes(value))
					.map(valueMapper);

				const removedItems = oldValues
					.filter(value => !newValues.includes(value))
					.map(valueMapper);

				this.selectedItems = newValues.map(valueMapper);

				this.tagNames = this._getTagNames();

				addedItems.forEach(item =>
					this._notifyItemsChanged(
						'itemAdded',
						this.addCallback,
						item
					)
				);

				removedItems.forEach(item =>
					this._notifyItemsChanged(
						'itemRemoved',
						this.removeCallback,
						item
					)
				);
			}
		});
	}

	/**
	 * Converts the list of selected tags into a comma-separated serialized
	 * version to be used as a fallback for old services and implementations.
	 *
	 * @private
	 * @return {string} The serialized, comma-separated version of the selected items.
	 */
	_getTagNames() {
		return this.selectedItems
			.map(selectedItem => selectedItem.value)
			.join();
	}

	/**
	 * Creates a tag with the text introduced in the input.
	 *
	 * @param  {!Event} event The event.
	 */
	_handleInputBlur(event) {
		const filteredItems = event.target.filteredItems;

		if (!filteredItems || (filteredItems && filteredItems.length === 0)) {
			const inputValue = event.target.inputValue;

			if (inputValue) {
				const existingTag = this.selectedItems.find(
					tag => tag.value === inputValue
				);

				if (existingTag) {
					return;
				}

				const item = {
					label: inputValue,
					value: inputValue
				};

				this.selectedItems = this.selectedItems.concat(item);
				this.tagNames = this._getTagNames();

				this._notifyItemsChanged('itemAdded', this.addCallback, item);
			}
		}
	}

	_handleInputFocus(event) {
		this.emit('inputFocus', event);
	}

	/**
	 * Updates tags fallback and notifies that a new tag has been added.
	 *
	 * @param {!Event} event The event.
	 * @private
	 */
	_handleItemAdded(event) {
		this.selectedItems = event.data.selectedItems;
		this.tagNames = this._getTagNames();

		this._notifyItemsChanged(
			'itemAdded',
			this.addCallback,
			event.data.item
		);
	}

	/**
	 * Updates tags fallback and notifies that a new tag has been removed.
	 *
	 * @param {!Event} event The event.
	 * @private
	 */
	_handleItemRemoved(event) {
		this.selectedItems = event.data.selectedItems;
		this.tagNames = this._getTagNames();

		this._notifyItemsChanged(
			'itemRemoved',
			this.removeCallback,
			event.data.item
		);
	}

	/**
	 * Responds to user input to retrieve the list of available tags from the
	 * tags search service.
	 *
	 * @param {!string} query
	 * @private
	 */
	_handleQuery(query) {
		return new Promise(resolve => {
			Liferay.Service(
				'/assettag/search',
				{
					end: 20,
					groupIds: this.groupIds,
					name: `%${query === '*' ? '' : query}%`,
					start: 0,
					tagProperties: ''
				},
				tags => resolve(tags.map(tag => tag.value))
			);
		});
	}

	/**
	 * Notifies changed items
	 *
	 * @param {!string} eventName
	 * @param {!Function} callback
	 * @param {!object} item
	 * @private
	 */
	_notifyItemsChanged(eventName, callback, item) {
		if (callback) {
			window[callback](item);
		}

		this.emit(eventName, {
			item,
			selectedItems: this.selectedItems
		});
	}
}

/**
 * State definition.
 *
 * @static
 * @type {!Object}
 */
AssetTagsSelector.STATE = {
	/**
	 * Function to call every time the input value changes.
	 *
	 * @default _handleQuery
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?func}
	 */
	_dataSource: Config.func().internal(),

	/**
	 * Function to call when a tag is added.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?string}
	 */
	addCallback: Config.string(),

	/**
	 * Event name which fires when the user selects a display page using the
	 * item selector.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?string}
	 */
	eventName: Config.string(),

	/**
	 * List of group IDs where tags should be located.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?string}
	 */
	groupIds: Config.array().value([]),

	/**
	 * URL of a portlet to display the tags.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?string}
	 */
	portletURL: Config.string(),

	/**
	 * Function to call when a tag is removed.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?string}
	 */
	removeCallback: Config.string(),

	/**
	 * List of the selected items.
	 *
	 * @default []
	 * @instance
	 * @memberof AssetTagsSelector
	 * @type {?Array<Object>}
	 */
	selectedItems: Config.array(Config.object()).value([]),

	/**
	 * A comma separated list of selected items.
	 *
	 * @default undefined
	 * @instance
	 * @memberof AssetTagsSelector
	 * @review
	 * @type {?string}
	 */
	tagNames: Config.string().value('')
};

Soy.register(AssetTagsSelector, templates);

export {AssetTagsSelector};
export default AssetTagsSelector;