Your IP : 3.17.75.47


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/ui/entity-selector/src/dialog/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/ui/entity-selector/src/dialog/dialog.js

import { Type, Text, Tag, Dom, ajax as Ajax, Cache, Loc, Runtime, Reflection } from 'main.core';
import { EventEmitter, BaseEvent } from 'main.core.events';
import { Popup } from 'main.popup';
import { Loader } from 'main.loader';

import Item from '../item/item';
import Tab from './tabs/tab';
import Entity from '../entity/entity';
import TagSelector from '../tag-selector/tag-selector';
import Navigation from './navigation';
import SliderIntegration from './integration/slider-integration';
import Animation from '../common/animation';
import BaseHeader from './header/base-header';
import DefaultHeader from './header/default-header';
import BaseFooter from './footer/base-footer';
import DefaultFooter from './footer/default-footer';

import RecentTab from './tabs/recent-tab';
import SearchTab from './tabs/search-tab';

import type ItemNode from '../item/item-node';
import type { TabOptions } from './tabs/tab-options';
import type { DialogOptions } from './dialog-options';
import type { ItemOptions } from '../item/item-options';
import type { EntityOptions } from '../entity/entity-options';
import type { ItemId } from '../item/item-id';
import type { PopupOptions } from 'main.popup';
import type { HeaderOptions, HeaderContent } from './header/header-content';
import type { FooterOptions, FooterContent } from './footer/footer-content';
import type { ItemNodeOptions } from '../item/item-node-options';
import type { TagItemOptions } from '../tag-selector/tag-item-options';

class LoadState
{
	static UNSENT: string = 'UNSENT';
	static LOADING: string = 'LOADING';
	static DONE: string = 'DONE';
}

class TagSelectorMode
{
	static INSIDE: string = 'INSIDE';
	static OUTSIDE: string = 'OUTSIDE';
}

const instances = new Map();

/**
 * @memberof BX.UI.EntitySelector
 */
export default class Dialog extends EventEmitter
{
	id: string = null;
	items: Map<string, Map<string, Item>> = new Map();
	tabs: Map<string, Entity> = new Map();
	entities: Map<string, Entity> = new Map();
	targetNode: HTMLElement = null;
	popup: Popup = null;
	cache = new Cache.MemoryCache();
	multiple: boolean = true;
	hideOnSelect: boolean = null;
	hideOnDeselect: boolean = null;
	addTagOnSelect: boolean = null;
	clearSearchOnSelect: boolean = true;
	context: string = null;
	selectedItems: Set<Item> = new Set();
	preselectedItems: ItemId[] = [];
	undeselectedItems: ItemId[] = [];
	dropdownMode: boolean = false;

	frozen: boolean = false;
	frozenProps: { [propName: string]: any } = {};

	hideByEsc: boolean = true;
	autoHide: boolean = true;
	autoHideHandler: Function = null;
	offsetTop: number = 5;
	offsetLeft: number = 0;
	cacheable: boolean = true;

	width: number = 565;
	height: number = 420;

	maxLabelWidth: number = 160;
	minLabelWidth: number = 45;

	showAvatars: boolean = true;
	compactView: boolean = false;

	activeTab: Tab = null;
	recentTab: Tab = null;
	searchTab: Tab = null;

	rendered: boolean = false;

	loadState: LoadState = LoadState.UNSENT;
	loader: ?Loader = null;

	tagSelector: ?TagSelector = null;
	tagSelectorMode: ?TagSelectorMode = null;
	tagSelectorHeight: ?number = null;

	saveRecentItemsWithDebounce: Function = Runtime.debounce(this.saveRecentItems, 2000, this);
	recentItemsToSave = [];
	recentItemsLimit: ?number = null;

	navigation: Navigation = null;
	header: BaseHeader = null;
	footer: BaseFooter = null;
	popupOptions: PopupOptions = {};

	focusOnFirst: boolean = true;
	focusedNode: ItemNode = null;

	clearUnavailableItems: boolean = false;
	overlappingObserver: MutationObserver = null;
	offsetAnimation: boolean = true;

	static getById(id: string): ?Dialog
	{
		return instances.get(id) || null;
	}

	static getInstances(): Dialog[]
	{
		return Array.from(instances.values());
	}

	constructor(dialogOptions: DialogOptions)
	{
		super();
		this.setEventNamespace('BX.UI.EntitySelector.Dialog');

		const options: DialogOptions = Type.isPlainObject(dialogOptions) ? dialogOptions : {};
		this.id = Type.isStringFilled(options.id) ? options.id : `ui-selector-${Text.getRandom().toLowerCase()}`;
		this.multiple = Type.isBoolean(options.multiple) ? options.multiple : true;
		this.context = Type.isStringFilled(options.context) ? options.context : null;
		this.clearUnavailableItems = options.clearUnavailableItems === true;
		this.compactView = options.compactView === true;
		this.dropdownMode = Type.isBoolean(options.dropdownMode) ? options.dropdownMode : false;

		if (Type.isArray(options.entities))
		{
			options.entities.forEach((entity) => {
				this.addEntity(entity);
			});
		}

		if (options.tagSelector instanceof TagSelector)
		{
			this.tagSelectorMode = TagSelectorMode.OUTSIDE;
			this.setTagSelector(options.tagSelector);
		}
		else if (options.enableSearch === true)
		{
			const defaultOptions = {
				placeholder: Loc.getMessage('UI_TAG_SELECTOR_SEARCH_PLACEHOLDER'),
				maxHeight: 102, // three lines
				textBoxWidth: 105,
			};
			const customOptions = Type.isPlainObject(options.tagSelectorOptions) ? options.tagSelectorOptions : {};
			const mandatoryOptions = {
				dialogOptions: null,
				showTextBox: true,
				showAddButton: false,
				showCreateButton: false,
				multiple: this.isMultiple(),
			};

			const tagSelectorOptions = Object.assign(defaultOptions, customOptions, mandatoryOptions);
			const tagSelector = new TagSelector(tagSelectorOptions);
			this.tagSelectorMode = TagSelectorMode.INSIDE;
			this.setTagSelector(tagSelector);
		}

		this.setTargetNode(options.targetNode);
		this.setHideOnSelect(options.hideOnSelect);
		this.setHideOnDeselect(options.hideOnDeselect);
		this.setAddTagOnSelect(options.addTagOnSelect);
		this.setClearSearchOnSelect(options.clearSearchOnSelect);
		this.setWidth(options.width);
		void this.setHeight(options.height);
		this.setAutoHide(options.autoHide);
		this.setAutoHideHandler(options.autoHideHandler);
		this.setHideByEsc(options.hideByEsc);
		this.setOffsetLeft(options.offsetLeft);
		this.setOffsetTop(options.offsetTop);
		this.setCacheable(options.cacheable);
		this.setFocusOnFirst(options.focusOnFirst);
		this.setShowAvatars(options.showAvatars);
		this.setRecentItemsLimit(options.recentItemsLimit);
		this.setOffsetAnimation(options.offsetAnimation);

		this.recentTab = new RecentTab(this, options.recentTabOptions);
		this.searchTab = new SearchTab(this, options.searchTabOptions, options.searchOptions);

		this.addTab(this.recentTab);
		this.addTab(this.searchTab);

		this.setPreselectedItems(options.preselectedItems);
		this.setUndeselectedItems(options.undeselectedItems);

		this.setOptions(options);

		const preload = options.preload === true || this.getPreselectedItems().length > 0;
		if (preload)
		{
			this.load();
		}

		if (Type.isPlainObject(options.popupOptions))
		{
			const allowedOptions = new Set([
				'overlay',
				'bindOptions',
				'targetContainer',
				'zIndexOptions',
				'events',
				'animation',
				'className',
			]);

			const popupOptions = {};

			Object.keys(options.popupOptions).forEach((option: string) => {
				if (allowedOptions.has(option))
				{
					popupOptions[option] = options.popupOptions[option];
				}
			});

			this.popupOptions = popupOptions;
		}

		this.navigation = new Navigation(this);

		(new SliderIntegration(this));

		this.subscribe('ItemNode:onFocus', this.handleItemNodeFocus.bind(this));
		this.subscribe('ItemNode:onUnfocus', this.handleItemNodeUnfocus.bind(this));

		this.subscribeFromOptions(options.events);

		instances.set(this.id, this);
	}

	show(): void
	{
		this.load();
		this.getPopup().show();
	}

	hide(): void
	{
		this.getPopup().close();
	}

	destroy(): void
	{
		if (this.destroying)
		{
			return;
		}

		this.destroying = true;
		this.emit('onDestroy');

		this.disconnectTabOverlapping();
		instances.delete(this.getId());
		if (this.isRendered())
		{
			this.getPopup().destroy();
		}

		for (const property in this)
		{
			if (this.hasOwnProperty(property))
			{
				delete this[property];
			}
		}

		Object.setPrototypeOf(this, null);
	}

	isOpen(): boolean
	{
		return this.popup && this.popup.isShown();
	}

	adjustPosition(): void
	{
		if (this.isRendered())
		{
			this.getPopup().adjustPosition();
		}
	}

	search(queryString: string): void
	{
		const query = Type.isStringFilled(queryString) ? queryString.trim() : '';

		const event = new BaseEvent({ data: { query } });
		this.emit('onBeforeSearch', event);
		if (event.isDefaultPrevented())
		{
			return;
		}

		if (!Type.isStringFilled(query))
		{
			this.selectFirstTab();
			if (this.getSearchTab())
			{
				this.getSearchTab().clearResults();
			}
		}
		else if (this.getSearchTab())
		{
			this.selectTab(this.getSearchTab().getId());
			this.getSearchTab().search(query);
		}

		this.emit('onSearch', { query });
	}

	addItem(options: ItemOptions): Item
	{
		if (!Type.isPlainObject(options))
		{
			throw new Error('EntitySelector.addItem: wrong item options.');
		}

		let item = this.getItem(options);
		if (!item)
		{
			item = new Item(options);

			const undeselectable = this.getUndeselectedItems().some((itemId: ItemId) => {
				return itemId[0] === item.getEntityId() && String(itemId[1]) === String(item.getId());
			});

			if (undeselectable)
			{
				item.setDeselectable(false);
			}

			item.setDialog(this);

			const entity = this.getEntity(item.getEntityId());
			if (entity === null)
			{
				this.addEntity({ id: item.getEntityId() });
			}

			let entityItems = this.items.get(item.getEntityId());
			if (!entityItems)
			{
				entityItems = new Map();
				this.items.set(item.getEntityId(), entityItems);
			}

			entityItems.set(String(item.getId()), item);

			if (item.isSelected())
			{
				this.handleItemSelect(item);
			}
		}

		let tabs = [];
		if (Type.isArray(options.tabs))
		{
			tabs = options.tabs;
		}
		else if (Type.isStringFilled(options.tabs))
		{
			tabs = [options.tabs];
		}

		const children = Type.isArray(options.children) ? options.children : [];

		tabs.forEach((tabId) => {
			const tab = this.getTab(tabId);
			if (tab)
			{
				const itemNode = tab.getRootNode().addItem(item, options.nodeOptions);
				itemNode.addChildren(children);
			}
		});

		return item;
	}

	removeItem(item: Item | ItemOptions): ?Item
	{
		item = this.getItem(item);
		if (item)
		{
			this.handleItemDeselect(item);

			item.getNodes().forEach((node: ItemNode) => {
				node.getParentNode().removeChild(node);
			});

			const entityItems = this.getEntityItemsInternal(item.getEntityId());
			if (entityItems)
			{
				entityItems.delete(String(item.getId()));
				if (entityItems.size === 0)
				{
					this.items.delete(item.getEntityId());
				}
			}
		}

		return item;
	}

	removeItems(): void
	{
		this.getItemsInternal().forEach((items: Map<string, Item>) => {
			items.forEach((item: Item) => {
				this.removeItem(item);
			});
		});
	}

	getItem(item: ItemId | Item | ItemOptions): ?Item
	{
		let id = null;
		let entityId = null;

		if (Type.isArray(item) && item.length === 2)
		{
			[entityId, id] = item;
		}
		else if (item instanceof Item)
		{
			id = item.getId();
			entityId = item.getEntityId();
		}
		else if (Type.isObjectLike(item))
		{
			({ id, entityId } = item);
		}

		const entityItems = this.getEntityItemsInternal(entityId);
		if (entityItems)
		{
			return entityItems.get(String(id)) || null;
		}

		return null;
	}

	getSelectedItems(): Item[]
	{
		return Array.from(this.selectedItems);
	}

	getItems(): Item[]
	{
		const items = [];
		this.getItemsInternal().forEach((entityItems: Map<string, Item>) => {
			Array.prototype.push.apply(items, Array.from(entityItems.values()));
		});

		return items;
	}

	/**
	 * @internal
	 */
	getItemsInternal(): Map<string, Map<string, Item>>
	{
		return this.items;
	}

	getEntityItems(entityId: string): Item[]
	{
		const items = this.getEntityItemsInternal(entityId);

		return items === null ? [] : Array.from(items.values());
	}

	/**
	 * @internal
	 */
	getEntityItemsInternal(entityId: string): Map<string, Item> | null
	{
		return this.items.get(entityId) || null;
	}

	/**
	 * @private
	 */
	validateItemIds(itemIds: ItemId[]): ItemId[]
	{
		if (!Type.isArrayFilled(itemIds))
		{
			return [];
		}

		const result = [];
		itemIds.forEach((itemId: ItemId) => {
			if (!Type.isArray(itemId) || itemId.length !== 2)
			{
				return;
			}

			const [entityId, id] = itemId;

			if (Type.isStringFilled(entityId) && (Type.isStringFilled(id) || Type.isNumber(id)))
			{
				result.push(itemId);
			}
		});

		return result;
	}

	addTab(tab: Tab | TabOptions): Tab
	{
		if (Type.isPlainObject(tab))
		{
			tab = new Tab(this, tab);
		}

		if (!(tab instanceof Tab))
		{
			throw new Error('EntitySelector: a tab must be an instance of EntitySelector.Tab.');
		}

		if (this.getTab(tab.getId()))
		{
			console.error(`EntitySelector: the "${tab.getId()}" tab is already existed.`);
			return tab;
		}

		tab.setDialog(this);
		this.tabs.set(tab.getId(), tab);

		if (this.isRendered())
		{
			this.insertTab(tab);
		}

		return tab;
	}

	getTabs(): Tab[]
	{
		return Array.from(this.tabs.values());
	}

	getTab(id: string): ?Tab
	{
		return this.tabs.get(id) || null;
	}

	getRecentTab(): RecentTab
	{
		return this.recentTab;
	}

	getSearchTab(): SearchTab
	{
		return this.searchTab;
	}

	selectTab(id: string): ?Tab
	{
		const newActiveTab = this.getTab(id);
		if (!newActiveTab || newActiveTab === this.getActiveTab())
		{
			return newActiveTab;
		}

		if (this.getActiveTab())
		{
			this.getActiveTab().deselect();
		}

		this.activeTab = newActiveTab;
		newActiveTab.select();

		if (!newActiveTab.isRendered())
		{
			newActiveTab.render();
		}

		requestAnimationFrame(() => {
			requestAnimationFrame(() => {
				this.focusSearch();
			});
		});

		this.clearNodeFocus();
		if (this.shouldFocusOnFirst())
		{
			this.focusOnFirstNode();
		}

		this.adjustHeader();
		this.adjustFooter();

		return newActiveTab;
	}

	/**
	 * @private
	 */
	insertTab(tab: Tab): void
	{
		tab.renderLabel();
		tab.renderContainer();

		Dom.append(tab.getLabelContainer(), this.getLabelsContainer());
		Dom.append(tab.getContainer(), this.getTabContentsContainer());

		if (tab.getHeader())
		{
			Dom.append(tab.getHeader().getContainer(), this.getHeaderContainer());
		}

		if (tab.getFooter())
		{
			Dom.append(tab.getFooter().getContainer(), this.getFooterContainer());
		}
	}

	selectFirstTab(onlyVisible = true): ?Tab
	{
		const tabs = this.getTabs();
		for (let i = 0; i < tabs.length; i++)
		{
			const tab = tabs[i];
			if (onlyVisible === false || tab.isVisible())
			{
				return this.selectTab(tab.getId());
			}
		}

		if (this.isDropdownMode())
		{
			return this.selectTab(this.getRecentTab().getId());
		}

		return null;
	}

	selectLastTab(onlyVisible = true): ?Tab
	{
		const tabs = this.getTabs();
		for (let i = tabs.length - 1; i >= 0; i--)
		{
			const tab = tabs[i];
			if (onlyVisible === false || tab.isVisible())
			{
				return this.selectTab(tab.getId());
			}
		}

		if (this.isDropdownMode())
		{
			return this.selectTab(this.getRecentTab().getId());
		}

		return null;
	}

	getActiveTab(): ?Tab
	{
		return this.activeTab;
	}

	getNextTab(onlyVisible = true): ?Tab
	{
		let nextTab = null;
		let activeFound = false;
		const tabs = this.getTabs();
		for (let i =  0; i < tabs.length; i++)
		{
			const tab = tabs[i];
			if (onlyVisible && !tab.isVisible())
			{
				continue;
			}

			if (tab === this.getActiveTab())
			{
				activeFound = true;
			}
			else if (activeFound)
			{
				nextTab = tab;
				break;
			}
		}

		return nextTab;
	}

	getPreviousTab(onlyVisible = true): ?Tab
	{
		let previousTab = null;
		let activeFound = false;
		const tabs = this.getTabs();
		for (let i = tabs.length - 1; i >= 0; i--)
		{
			const tab = tabs[i];
			if (onlyVisible && !tab.isVisible())
			{
				continue;
			}

			if (tab === this.getActiveTab())
			{
				activeFound = true;
			}
			else if (activeFound)
			{
				previousTab = tab;
				break;
			}
		}

		return previousTab;
	}

	removeTab(id: string): void
	{
		const tab = this.getTab(id);
		if (!tab)
		{
			return;
		}

		tab.getRootNode().removeChildren();

		this.tabs.delete(id);

		Dom.remove(tab.getLabelContainer(), this.getLabelsContainer());
		Dom.remove(tab.getContainer(), this.getTabContentsContainer());

		if (tab.getHeader())
		{
			Dom.remove(tab.getHeader().getContainer(), this.getHeaderContainer());
		}

		if (tab.getFooter())
		{
			Dom.remove(tab.getFooter().getContainer(), this.getFooterContainer());
		}

		this.selectFirstTab();
	}

	addEntity(entity: Entity | EntityOptions): Entity
	{
		if (Type.isPlainObject(entity))
		{
			entity = new Entity(entity);
		}

		if (!(entity instanceof Entity))
		{
			throw new Error('EntitySelector: an entity must be an instance of EntitySelector.Entity.');
		}

		if (this.hasEntity(entity.getId()))
		{
			console.error(`EntitySelector: the "${entity.getId()}" entity is already existed.`);
			return entity;
		}

		this.entities.set(entity.getId(), entity);

		return entity;
	}

	getEntity(id: string): ?Entity
	{
		return this.entities.get(id) || null;
	}

	hasEntity(id: string): boolean
	{
		return this.entities.has(id);
	}

	getEntities(): Entity[]
	{
		return Array.from(this.entities.values());
	}

	removeEntity(id: string): void
	{
		this.removeEntityItems(id);
		this.entities.delete(id);
	}

	removeEntityItems(id: string): void
	{
		const items = this.getEntityItemsInternal(id);
		if (items)
		{
			items.forEach((item: Item) => {
				this.removeItem(item);
			});
		}
	}

	getHeader(): ?BaseHeader
	{
		return this.header;
	}

	getActiveHeader(): ?BaseHeader
	{
		if (!this.getActiveTab())
		{
			return null;
		}

		if (this.getActiveTab().getHeader())
		{
			return this.getActiveTab().getHeader();
		}

		return this.getHeader() && this.getActiveTab().canShowDefaultHeader() ? this.getHeader() : null;
	}

	/**
	 * @internal
	 */
	adjustHeader(): void
	{
		if (!this.getActiveTab())
		{
			return;
		}

		if (this.getActiveTab().getHeader())
		{
			if (this.getHeader())
			{
				this.getHeader().hide();
			}

			this.getActiveTab().getHeader().show();
		}
		else
		{
			if (this.getHeader())
			{
				if (this.getActiveTab().canShowDefaultHeader())
				{
					this.getHeader().show();
				}
				else
				{
					this.getHeader().hide();
				}
			}
		}
	}

	setHeader(headerContent: ?HeaderContent, headerOptions?: HeaderOptions): ?BaseHeader
	{
		/** @var {BaseHeader} */
		let header = null;
		if (headerContent !== null)
		{
			header = this.constructor.createHeader(this, headerContent, headerOptions);
			if (header === null)
			{
				return null;
			}
		}

		if (this.isRendered() && this.getHeader() !== null)
		{
			Dom.remove(this.getHeader().getContainer());
			this.adjustHeader();
		}

		this.header = header;

		if (this.isRendered())
		{
			this.appendHeader(header);
			this.adjustHeader();
		}

		return header;
	}

	/**
	 * @internal
	 */
	appendHeader(header: BaseHeader): void
	{
		if (header instanceof BaseHeader)
		{
			Dom.append(header.getContainer(), this.getHeaderContainer());
		}
	}

	/**
	 * @internal
	 */
	static createHeader(context: Dialog | Tab, headerContent: HeaderContent, headerOptions?: HeaderOptions): ?BaseHeader
	{
		if (
			!Type.isStringFilled(headerContent) &&
			!Type.isArrayFilled(headerContent) &&
			!Type.isDomNode(headerContent) &&
			!Type.isFunction(headerContent)
		)
		{
			return null;
		}

		/** @var {BaseHeader} */
		let header = null;
		const options = Type.isPlainObject(headerOptions) ? headerOptions : {};

		if (Type.isFunction(headerContent) || Type.isString(headerContent))
		{
			const className = Type.isString(headerContent) ? Reflection.getClass(headerContent) : headerContent;
			if (Type.isFunction(className))
			{
				header = new className(context, options);
				if (!(header instanceof BaseHeader))
				{
					console.error('EntitySelector: header is not an instance of BaseHeader.');
					header = null;
				}
			}
		}

		if (headerContent !== null && !header)
		{
			header = new DefaultHeader(context, Object.assign({}, options, { content: headerContent }));
		}

		return header;
	}

	getFooter(): ?BaseFooter
	{
		return this.footer;
	}

	getActiveFooter(): ?BaseFooter
	{
		if (!this.getActiveTab())
		{
			return null;
		}

		if (this.getActiveTab().getFooter())
		{
			return this.getActiveTab().getFooter();
		}

		return this.getFooter() && this.getActiveTab().canShowDefaultFooter() ? this.getFooter() : null;
	}

	/**
	 * @internal
	 */
	adjustFooter(): void
	{
		if (!this.getActiveTab())
		{
			return;
		}

		if (this.getActiveTab().getFooter())
		{
			if (this.getFooter())
			{
				this.getFooter().hide();
			}

			this.getActiveTab().getFooter().show();
		}
		else
		{
			if (this.getFooter())
			{
				if (this.getActiveTab().canShowDefaultFooter())
				{
					this.getFooter().show();
				}
				else
				{
					this.getFooter().hide();
				}
			}
		}
	}

	setFooter(footerContent: ?FooterContent, footerOptions?: FooterOptions): ?BaseFooter
	{
		/** @var {BaseFooter} */
		let footer = null;
		if (footerContent !== null)
		{
			footer = this.constructor.createFooter(this, footerContent, footerOptions);
			if (footer === null)
			{
				return null;
			}
		}

		if (this.isRendered() && this.getFooter() !== null)
		{
			Dom.remove(this.getFooter().getContainer());
			this.adjustFooter();
		}

		this.footer = footer;

		if (this.isRendered())
		{
			this.appendFooter(footer);
			this.adjustFooter();
		}

		return footer;
	}

	/**
	 * @internal
	 */
	appendFooter(footer: BaseFooter): void
	{
		if (footer instanceof BaseFooter)
		{
			Dom.append(footer.getContainer(), this.getFooterContainer());
		}
	}

	/**
	 * @internal
	 */
	static createFooter(context: Dialog | Tab, footerContent: FooterContent, footerOptions?: FooterOptions): ?BaseFooter
	{
		if (
			!Type.isStringFilled(footerContent) &&
			!Type.isArrayFilled(footerContent) &&
			!Type.isDomNode(footerContent) &&
			!Type.isFunction(footerContent)
		)
		{
			return null;
		}

		/** @var {BaseFooter} */
		let footer = null;
		const options = Type.isPlainObject(footerOptions) ? footerOptions : {};

		if (Type.isFunction(footerContent) || Type.isString(footerContent))
		{
			const className = Type.isString(footerContent) ? Reflection.getClass(footerContent) : footerContent;
			if (Type.isFunction(className))
			{
				footer = new className(context, options);
				if (!(footer instanceof BaseFooter))
				{
					console.error('EntitySelector: footer is not an instance of BaseFooter.');
					footer = null;
				}
			}
		}

		if (footerContent !== null && !footer)
		{
			footer = new DefaultFooter(context, Object.assign({}, options, { content: footerContent }));
		}

		return footer;
	}

	getId(): string
	{
		return this.id;
	}

	getContext(): ?string
	{
		return this.context;
	}

	getNavigation(): Navigation
	{
		return this.navigation;
	}

	deselectAll(): void
	{
		this.getSelectedItems().forEach((item: Item) => {
			item.deselect();
		});
	}

	isMultiple(): boolean
	{
		return this.multiple;
	}

	setTargetNode(node: HTMLElement | { left: number, top: number } | null | MouseEvent): void
	{
		if (!Type.isDomNode(node) && !Type.isNull(node) && !Type.isObject(node))
		{
			return;
		}

		this.targetNode = node;

		if (this.isRendered())
		{
			this.getPopup().setBindElement(this.targetNode);
			this.getPopup().adjustPosition();
		}
	}

	getTargetNode(): ?HTMLElement
	{
		if (this.targetNode === null)
		{
			if (this.getTagSelectorMode() === TagSelectorMode.OUTSIDE)
			{
				return this.getTagSelector().getOuterContainer();
			}
		}

		return this.targetNode;
	}

	setHideOnSelect(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.hideOnSelect = flag;
		}
	}

	shouldHideOnSelect(): boolean
	{
		if (this.hideOnSelect !== null)
		{
			return this.hideOnSelect;
		}

		return !this.isMultiple();
	}

	setHideOnDeselect(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.hideOnDeselect = flag;
		}
	}

	shouldHideOnDeselect(): boolean
	{
		if (this.hideOnDeselect !== null)
		{
			return this.hideOnDeselect;
		}

		return false;
	}

	setClearSearchOnSelect(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.clearSearchOnSelect = flag;
		}
	}

	shouldClearSearchOnSelect(): boolean
	{
		return this.clearSearchOnSelect;
	}

	setAddTagOnSelect(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.addTagOnSelect = flag;
		}
	}

	shouldAddTagOnSelect(): boolean
	{
		if (this.addTagOnSelect !== null)
		{
			return this.addTagOnSelect;
		}

		return this.isMultiple() || this.isTagSelectorOutside();
	}

	setShowAvatars(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.showAvatars = flag;

			if (this.isRendered())
			{
				this.getTabs().forEach((tab: Tab) => {
					tab.renderContainer();
				});
			}
		}
	}

	shouldShowAvatars(): boolean
	{
		return this.showAvatars;
	}

	setRecentItemsLimit(recentItemsLimit: number): void
	{
		if (Type.isNumber(recentItemsLimit) && recentItemsLimit > 0)
		{
			this.recentItemsLimit = recentItemsLimit;
		}
	}

	getRecentItemsLimit(): ?number
	{
		return this.recentItemsLimit;
	}

	setOffsetAnimation(flag: boolean): any
	{
		if (Type.isBoolean(flag))
		{
			this.offsetAnimation = flag;

			if (this.isRendered() && !this.offsetAnimation)
			{
				Dom.removeClass(this.getPopup().getPopupContainer(), 'ui-selector-popup-offset-animation');
			}
		}
	}

	isCompactView(): boolean
	{
		return this.compactView;
	}

	setAutoHide(enable: boolean): void
	{
		if (Type.isBoolean(enable))
		{
			this.autoHide = enable;
			if (this.isRendered())
			{
				this.getPopup().setAutoHide(enable);
			}
		}
	}

	isAutoHide(): boolean
	{
		return this.autoHide;
	}

	setAutoHideHandler(handler?: (event: MouseEvent, dialog: Dialog) => boolean): void
	{
		if (Type.isFunction(handler) || handler === null)
		{
			this.autoHideHandler = handler;
		}
	}

	setHideByEsc(enable: boolean): void
	{
		if (Type.isBoolean(enable))
		{
			this.hideByEsc = enable;
			if (this.isRendered())
			{
				this.getPopup().setClosingByEsc(enable);
			}
		}
	}

	shouldHideByEsc(): boolean
	{
		return this.hideByEsc;
	}

	getWidth(): number
	{
		return this.width;
	}

	setWidth(width: number): void
	{
		if (Type.isNumber(width) && width > 0)
		{
			this.width = width;
			if (this.isRendered())
			{
				Dom.style(this.getContainer(), 'width', `${width}px`);
			}
		}
	}

	getHeight(): number
	{
		return this.height;
	}

	setHeight(height: number): Promise
	{
		if (Type.isNumber(height) && height > 0)
		{
			this.height = height;
			if (this.isRendered())
			{
				Dom.style(this.getContainer(), 'height', `${height}px`);
				return Animation.handleTransitionEnd(this.getContainer(), 'height');
			}
			else
			{
				return Promise.resolve();
			}
		}
		else
		{
			return Promise.resolve();
		}
	}

	getOffsetLeft(): number
	{
		return this.offsetLeft;
	}

	setOffsetLeft(offset: number): void
	{
		if (Type.isNumber(offset) && offset >= 0)
		{
			this.offsetLeft = offset;
			if (this.isRendered())
			{
				this.getPopup().setOffset({ offsetLeft: offset });
				this.adjustPosition();
			}
		}
	}

	getOffsetTop(): number
	{
		return this.offsetTop;
	}

	setOffsetTop(offset: number): void
	{
		if (Type.isNumber(offset) && offset >= 0)
		{
			this.offsetTop = offset;
			if (this.isRendered())
			{
				this.getPopup().setOffset({ offsetTop: offset });
				this.adjustPosition();
			}
		}
	}

	getZindex(): number
	{
		return this.getPopup().getZindex();
	}

	isCacheable(): boolean
	{
		return this.cacheable;
	}

	setCacheable(cacheable: boolean): void
	{
		if (Type.isBoolean(cacheable))
		{
			this.cacheable = cacheable;
			if (this.isRendered())
			{
				this.getPopup().setCacheable(cacheable);
			}
		}
	}

	shouldFocusOnFirst(): boolean
	{
		return this.focusOnFirst;
	}

	setFocusOnFirst(flag: boolean): void
	{
		if (Type.isBoolean(flag))
		{
			this.focusOnFirst = flag;
		}
	}

	focusOnFirstNode(): ?ItemNode
	{
		if (this.getActiveTab())
		{
			const itemNode = this.getActiveTab().getRootNode().getFirstChild();
			if (itemNode)
			{
				itemNode.focus();

				return itemNode;
			}
		}

		return null;
	}

	getFocusedNode(): ?ItemNode
	{
		return this.focusedNode;
	}

	clearNodeFocus(): void
	{
		if (this.focusedNode)
		{
			this.focusedNode.unfocus();
			this.focusedNode = null;
		}
	}

	isDropdownMode(): boolean
	{
		return this.dropdownMode;
	}

	setPreselectedItems(itemIds: ItemId[]): void
	{
		this.preselectedItems = this.validateItemIds(itemIds);
	}

	getPreselectedItems(): ItemId[]
	{
		return this.preselectedItems;
	}

	setUndeselectedItems(itemIds: ItemId[]): void
	{
		this.undeselectedItems = this.validateItemIds(itemIds);
	}

	getUndeselectedItems()
	{
		return this.undeselectedItems;
	}

	/**
	 * @private
	 */
	setOptions(dialogOptions: DialogOptions): void
	{
		const options = Type.isPlainObject(dialogOptions) ? dialogOptions : {};

		if (Type.isArray(options.tabs))
		{
			options.tabs.forEach((tab) => {
				this.addTab(tab);
			});
		}

		if (Type.isArray(options.selectedItems))
		{
			options.selectedItems.forEach((itemOptions: ItemOptions) => {
				const options = Object.assign({}, Type.isPlainObject(itemOptions) ? itemOptions : {});
				options.selected = true;
				this.addItem(options);
			});
		}

		if (Type.isArray(options.items))
		{
			options.items.forEach((itemOptions: ItemOptions) => {
				this.addItem(itemOptions);
			});
		}

		this.setHeader(options.header, options.headerOptions);
		this.setFooter(options.footer, options.footerOptions);
	}

	getMaxLabelWidth(): number
	{
		return this.maxLabelWidth;
	}

	getMinLabelWidth(): number
	{
		return this.minLabelWidth;
	}

	getTagSelector(): ?TagSelector
	{
		return this.tagSelector;
	}

	getTagSelectorMode(): ?TagSelectorMode
	{
		return this.tagSelectorMode;
	}

	isTagSelectorInside(): boolean
	{
		return this.getTagSelector() && this.getTagSelectorMode() === TagSelectorMode.INSIDE;
	}

	isTagSelectorOutside(): boolean
	{
		return this.getTagSelector() && this.getTagSelectorMode() === TagSelectorMode.OUTSIDE;
	}

	getTagSelectorQuery(): string
	{
		return this.getTagSelector() ? this.getTagSelector().getTextBoxValue() : '';
	}

	/**
	 * @private
	 */
	setTagSelector(tagSelector: TagSelector): void
	{
		this.tagSelector = tagSelector;

		this.tagSelector.subscribe('onInput', Runtime.debounce(this.handleTagSelectorInput, 200, this));
		this.tagSelector.subscribe('onAddButtonClick', this.handleTagSelectorAddButtonClick.bind(this));
		this.tagSelector.subscribe('onTagRemove', this.handleTagSelectorTagRemove.bind(this));
		this.tagSelector.subscribe('onAfterTagRemove', this.handleTagSelectorAfterTagRemove.bind(this));
		this.tagSelector.subscribe('onAfterTagAdd', this.handleTagSelectorAfterTagAdd.bind(this));
		this.tagSelector.subscribe('onContainerClick', this.handleTagSelectorClick.bind(this));

		this.tagSelector.setDialog(this);
	}

	focusSearch(): void
	{
		if (this.getTagSelector())
		{
			if (this.getActiveTab() !== this.getSearchTab())
			{
				this.getTagSelector().clearTextBox();
			}

			this.getTagSelector().focusTextBox();
		}
	}

	clearSearch(): void
	{
		if (this.getTagSelector())
		{
			this.getTagSelector().clearTextBox();

			if (this.getActiveTab() === this.getSearchTab())
			{
				this.selectFirstTab();
			}
		}
	}

	getLoader(): Loader
	{
		if (this.loader === null)
		{
			this.loader = new Loader({
				target: this.getTabsContainer(),
				size: 100
			});
		}

		return this.loader;
	}

	showLoader(): void
	{
		void this.getLoader().show();
	}

	hideLoader(): void
	{
		if (this.loader !== null)
		{
			void this.getLoader().hide();
		}
	}

	destroyLoader(): void
	{
		if (this.loader !== null)
		{
			this.getLoader().destroy();
		}

		this.loader = null;
	}

	getPopup(): Popup
	{
		if (this.popup !== null)
		{
			return this.popup;
		}

		this.getTabs().forEach((tab: Tab) => {
			this.insertTab(tab);
		});

		const popupOptions = { ...this.popupOptions };
		const userEvents = popupOptions.events;
		delete popupOptions.events;

		this.popup = new Popup({
			contentPadding: 0,
			padding: 0,
			offsetTop: this.getOffsetTop(),
			offsetLeft: this.getOffsetLeft(),
			animation: {
				showClassName: 'ui-selector-popup-animation-show',
				closeClassName: 'ui-selector-popup-animation-close',
				closeAnimationType: 'animation',
			},
			bindElement: this.getTargetNode(),
			bindOptions: {
				forceBindPosition: true,
			},
			autoHide: this.isAutoHide(),
			autoHideHandler: this.handleAutoHide.bind(this),
			closeByEsc: this.shouldHideByEsc(),
			cacheable: this.isCacheable(),
			events: {
				onFirstShow: this.handlePopupFirstShow.bind(this),
				onShow: this.handlePopupShow.bind(this),
				onAfterShow: this.handlePopupAfterShow.bind(this),
				onAfterClose: this.handlePopupAfterClose.bind(this),
				onDestroy: this.handlePopupDestroy.bind(this),
			},
			content: this.getContainer(),
			...popupOptions,
		});

		this.popup.subscribeFromOptions(userEvents);

		this.rendered = true;

		this.selectFirstTab();

		return this.popup;
	}

	isRendered(): boolean
	{
		return this.rendered;
	}

	getContainer(): HTMLElement
	{
		return this.cache.remember('container', () => {

			let searchContainer = '';
			if (this.getTagSelectorMode() === TagSelectorMode.INSIDE)
			{
				searchContainer = Tag.render`<div class="ui-selector-search"></div>`;

				this.getTagSelector().renderTo(searchContainer);
			}

			const className = this.isCompactView() ? ' ui-selector-dialog--compact-view' : '';

			return Tag.render`
				<div
					class="ui-selector-dialog${className}"
					style="width:${this.getWidth()}px; height:${this.getHeight()}px;"
				>
					${this.getHeaderContainer()}
					${searchContainer}
					${this.getTabsContainer()}
					${this.getFooterContainer()}
				</div>
			`;
		});
	}

	getTabsContainer(): HTMLElement
	{
		return this.cache.remember('tabs-container', () => {
			return Tag.render`
				<div class="ui-selector-tabs">
					${this.getTabContentsContainer()}
					${this.getLabelsContainer()}
				</div>
			`;
		});
	}

	getTabContentsContainer(): HTMLElement
	{
		return this.cache.remember('tab-contents', () => {
			return Tag.render`<div class="ui-selector-tab-contents"></div>`;
		});
	}

	getLabelsContainer(): HTMLElement
	{
		return this.cache.remember('labels-container', () => {
			return Tag.render`
				<div
					class="ui-selector-tab-labels"
					onmouseenter="${this.handleLabelsMouseEnter.bind(this)}"
					onmouseleave="${this.handleLabelsMouseLeave.bind(this)}"
				></div>
			`;
		});
	}

	getHeaderContainer(): HTMLElement
	{
		return this.cache.remember('header', () => {
			const header = this.getHeader() && this.getHeader().getContainer();

			return Tag.render`
				<div class="ui-selector-header-container">${header ? header : ''}</div>
			`;
		});
	}

	getFooterContainer(): HTMLElement
	{
		return this.cache.remember('footer', () => {
			const footer = this.getFooter() && this.getFooter().getContainer();

			return Tag.render`
				<div class="ui-selector-footer-container">${footer ? footer : ''}</div>
			`;
		});
	}

	freeze(): void
	{
		if (this.isFrozen())
		{
			return;
		}

		this.frozenProps = {
			autoHide: this.isAutoHide(),
			hideByEsc: this.shouldHideByEsc(),
		};

		this.setAutoHide(false);
		this.setHideByEsc(false);

		this.getNavigation().disable();
		Dom.addClass(this.getContainer(), 'ui-selector-dialog--freeze');

		this.frozen = true;
	}

	unfreeze(): void
	{
		if (!this.isFrozen())
		{
			return;
		}

		this.setAutoHide(this.frozenProps.autoHide !== false);
		this.setHideByEsc(this.frozenProps.hideByEsc !== false);

		this.getNavigation().enable();
		Dom.removeClass(this.getContainer(), 'ui-selector-dialog--freeze');

		this.frozen = false;
	}

	isFrozen(): boolean
	{
		return this.frozen;
	}

	hasRecentItems(): Promise
	{
		return new Promise((resolve, reject) => {
			Ajax
				.runAction('ui.entityselector.load', {
					json: {
						dialog: this.getAjaxJson()
					},
					getParameters: {
						context: this.getContext()
					}
				})
				.then((response) => {
					resolve(
						response.data && response.data.dialog && Type.isArrayFilled(response.data.dialog.recentItems)
					);
				})
				.catch((error) => {
					reject(error);
				})
			;
		});
	}

	load(): void
	{
		if (this.loadState !== LoadState.UNSENT || !this.hasDynamicLoad())
		{
			return;
		}

		if (this.getTagSelector())
		{
			this.getTagSelector().lock();
		}

		setTimeout(() => {
			if (this.isLoading())
			{
				this.showLoader();
			}
		}, 400);

		this.loadState = LoadState.LOADING;

		Ajax.runAction('ui.entityselector.load', {
				json: {
					dialog: this.getAjaxJson()
				},
				getParameters: {
					context: this.getContext()
				}
			})
			.then((response) => {
				if (response && response.data && Type.isPlainObject(response.data.dialog))
				{
					this.loadState = LoadState.DONE;

					const entities =
						Type.isArrayFilled(response.data.dialog.entities)
							? response.data.dialog.entities
							: []
					;

					entities.forEach((entityOptions: EntityOptions) => {
						const entity = this.getEntity(entityOptions.id);
						if (entity)
						{
							entity.setDynamicSearch(entityOptions.dynamicSearch);

						}
					});

					this.setOptions(response.data.dialog);

					this.getPreselectedItems().forEach((preselectedItem: ItemId) => {
						const item = this.getItem(preselectedItem);
						if (item)
						{
							item.select(true);
						}
					});

					const recentItems = response.data.dialog.recentItems;
					if (Type.isArray(recentItems))
					{
						const nodeOptionsMap: Map<Item, ItemNodeOptions> = new Map();
						const itemsOptions: ItemOptions[] = response.data.dialog.items;
						if (Type.isArray(itemsOptions))
						{
							itemsOptions.forEach((itemOptions: ItemOptions) => {
								if (itemOptions.nodeOptions)
								{
									const item = this.getItem(itemOptions);
									if (item)
									{
										nodeOptionsMap.set(item, itemOptions.nodeOptions);
									}
								}
							});
						}

						const items = recentItems.map((recentItem: ItemId) => {
							const item = this.getItem(recentItem);

							return [item, nodeOptionsMap.get(item)];
						});

						this.getRecentTab().getRootNode().addItems(items);
					}

					if (!this.getRecentTab().getRootNode().hasChildren() && this.getRecentTab().getStub())
					{
						this.getRecentTab().getStub().show();
					}

					if (this.getTagSelector())
					{
						this.getTagSelector().unlock();
					}

					if (this.isRendered())
					{
						if (this.isDropdownMode() && this.getActiveTab() === this.getRecentTab())
						{
							this.selectFirstTab();
						}
						else if (!this.getActiveTab())
						{
							this.selectFirstTab();
						}
					}

					this.focusSearch();
					this.destroyLoader();

					if (this.shouldFocusOnFirst())
					{
						this.focusOnFirstNode();
					}

					this.emit('onLoad');
				}
			})
			.catch((error) => {
				this.loadState = LoadState.UNSENT;

				if (this.getTagSelector())
				{
					this.getTagSelector().unlock();
				}

				this.focusSearch();
				this.destroyLoader();

				this.emit('onLoadError', { error });

				console.error(error);
			});
	}

	isLoaded(): boolean
	{
		return this.loadState === LoadState.DONE;
	}

	isLoading(): boolean
	{
		return this.loadState === LoadState.LOADING;
	}

	hasDynamicLoad(): boolean
	{
		let hasDynamicLoad = false;
		this.entities.forEach((entity: Entity) => {
			hasDynamicLoad = hasDynamicLoad || entity.hasDynamicLoad();
		});

		return hasDynamicLoad;
	}

	hasDynamicSearch(): boolean
	{
		let hasDynamicSearch = false;
		this.entities.forEach((entity: Entity) => {
			hasDynamicSearch = hasDynamicSearch || (entity.isSearchable() && entity.hasDynamicSearch());
		});

		return hasDynamicSearch;
	}

	saveRecentItem(item: Item): void
	{
		if (this.getContext() === null || !item.isSaveable())
		{
			return;
		}

		this.recentItemsToSave.push(item);
		this.saveRecentItemsWithDebounce();
	}

	/**
	 * @private
	 */
	saveRecentItems(): void
	{
		if (!Type.isArrayFilled(this.recentItemsToSave))
		{
			return;
		}

		Ajax.runAction('ui.entityselector.saveRecentItems', {
				json: {
					dialog: this.getAjaxJson(),
					recentItems: this.recentItemsToSave.map((item: Item) => item.getAjaxJson())
				},
				getParameters: {
					context: this.getContext()
				}
			})
			.then((response) => {

			})
			.catch((error) => {
				console.error(error);
			});

		this.recentItemsToSave = [];
	}

	shouldClearUnavailableItems(): boolean
	{
		return this.clearUnavailableItems;
	}

	/**
	 * @private
	 */
	handleTagSelectorInput(): void
	{
		if (this.getTagSelectorMode() === TagSelectorMode.OUTSIDE && !this.isOpen())
		{
			this.show();
		}

		const query = this.getTagSelector().getTextBoxValue();
		this.search(query);
	}

	/**
	 * @private
	 */
	handleTagSelectorAddButtonClick(): void
	{
		this.show();
	}

	/**
	 * @private
	 */
	handleTagSelectorTagRemove(event: BaseEvent): void
	{
		const { tag } = event.getData();

		const item = this.getItem({ id: tag.getId(), entityId: tag.getEntityId() });
		if (item)
		{
			item.deselect();
		}

		this.focusSearch();
	}

	/**
	 * @private
	 */
	handleTagSelectorAfterTagRemove(): void
	{
		this.adjustByTagSelector();
	}

	/**
	 * @private
	 */
	handleTagSelectorAfterTagAdd(): void
	{
		this.adjustByTagSelector();
	}

	/**
	 * @private
	 */
	adjustByTagSelector(): void
	{
		if (this.getTagSelectorMode() === TagSelectorMode.OUTSIDE)
		{
			this.adjustPosition();
		}
		else if (this.getTagSelectorMode() === TagSelectorMode.INSIDE)
		{
			const newTagSelectorHeight = this.getTagSelector().calcHeight();
			if (newTagSelectorHeight > 0)
			{
				const offset = newTagSelectorHeight - (this.tagSelectorHeight || this.getTagSelector().getMinHeight());
				this.tagSelectorHeight = newTagSelectorHeight;
				if (offset !== 0)
				{
					const height = this.getHeight();
					this.setHeight(height + offset).then(() => {
						this.adjustPosition();
					});
				}
			}
		}
	}

	/**
	 * @private
	 */
	handleTagSelectorClick(): void
	{
		this.focusSearch();
	}

	/**
	 * @internal
	 */
	handleItemSelect(item: Item, animate: boolean = true): void
	{
		const shouldAnimate: boolean =
			this.isMultiple()
				? animate
				: this.getSelectedItems().length === 0
		;

		if (!this.isMultiple())
		{
			this.deselectAll();

			if (this.getSelectedItems().length > 0)
			{
				console.error('EntitySelector: some items are still selected.', this.getSelectedItems());
			}
		}

		if (this.getTagSelector() && this.shouldAddTagOnSelect())
		{
			const tag: TagItemOptions = item.createTag();
			tag.animate = shouldAnimate;
			this.getTagSelector().addTag(tag);
		}

		this.selectedItems.add(item);
	}

	/**
	 * @internal
	 */
	handleItemDeselect(item: Item): void
	{
		const shouldAnimate: boolean = this.isMultiple();

		this.selectedItems.delete(item);

		if (this.getTagSelector())
		{
			this.getTagSelector().removeTag({
				id: item.getId(),
				entityId: item.getEntityId()
			}, shouldAnimate);
		}
	}

	/**
	 * @private
	 */
	handlePopupAfterShow(): void
	{
		this.focusSearch();
		this.adjustByTagSelector();

		this.emit('onShow');
	}

	/**
	 * @private
	 */
	handlePopupFirstShow(): void
	{
		this.emit('onFirstShow');

		this.observeTabOverlapping();
	}

	/**
	 * @private
	 */
	handlePopupShow(): void
	{
		if (this.offsetAnimation)
		{
			requestAnimationFrame(() => {
				requestAnimationFrame(() => {
					Dom.addClass(this.getPopup().getPopupContainer(), 'ui-selector-popup-offset-animation');
				});
			});
		}
	}

	/**
	 * @private
	 */
	handleAutoHide(event: MouseEvent): void
	{
		const target = event.target;
		const el = this.getPopup().getPopupContainer();
		if (target === el || el.contains(target))
		{
			return false;
		}

		if (
			this.isTagSelectorOutside()
			&& target === this.getTagSelector().getTextBox()
			&& Type.isStringFilled(this.getTagSelector().getTextBoxValue())
		)
		{
			return false;
		}

		if (this.autoHideHandler !== null)
		{
			const result = this.autoHideHandler(event, this);
			if (Type.isBoolean(result))
			{
				return result;
			}
		}

		return true;
	}

	/**
	 * @private
	 */
	observeTabOverlapping(): void
	{
		this.disconnectTabOverlapping();

		this.overlappingObserver = new MutationObserver(() => {
			if (this.getLabelsContainer().offsetWidth > 0)
			{
				const left = parseInt(this.getPopup().getPopupContainer().style.left, 10);
				if (left < this.getMinLabelWidth())
				{
					Dom.style(this.getPopup().getPopupContainer(), 'left', `${this.getMinLabelWidth()}px`);
				}
			}
		});

		this.overlappingObserver.observe(this.getPopup().getPopupContainer(), {
			attributes: true,
			attributeFilter: ['style']
		});
	}

	/**
	 * @private
	 */
	disconnectTabOverlapping(): void
	{
		if (this.overlappingObserver)
		{
			this.overlappingObserver.disconnect();
		}
	}

	/**
	 * @private
	 */
	handlePopupAfterClose(): void
	{
		if (this.isTagSelectorOutside())
		{
			if (this.getActiveTab() && this.getActiveTab() === this.getSearchTab())
			{
				this.selectFirstTab();
			}

			this.getTagSelector().clearTextBox();
			this.getTagSelector().showAddButton();
			this.getTagSelector().hideTextBox();
		}

		if (this.offsetAnimation)
		{
			Dom.removeClass(this.getPopup().getPopupContainer(), 'ui-selector-popup-offset-animation');
		}

		this.emit('onHide');
	}

	/**
	 * @private
	 */
	handlePopupDestroy(): void
	{
		this.destroy();
	}

	/**
	 * @private
	 */
	handleLabelsMouseEnter(): void
	{
		const rect = Dom.getRelativePosition(this.getLabelsContainer(), this.getPopup().getTargetContainer());
		const freeSpace = rect.right;

		if (freeSpace > this.getMinLabelWidth())
		{
			Dom.removeClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-hide');
			Dom.addClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-show');

			Dom.style(this.getLabelsContainer(), 'max-width', `${Math.min(freeSpace, this.getMaxLabelWidth())}px`);
			Animation.handleTransitionEnd(this.getLabelsContainer(), 'max-width').then(() => {
				Dom.removeClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-show');
				Dom.addClass(this.getLabelsContainer(), 'ui-selector-tab-labels--active');
			});
		}
		else
		{
			Dom.addClass(this.getLabelsContainer(), 'ui-selector-tab-labels--active');
		}
	}

	/**
	 * @private
	 */
	handleLabelsMouseLeave(): void
	{
		Dom.addClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-hide');
		Dom.removeClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-show');
		Dom.removeClass(this.getLabelsContainer(), 'ui-selector-tab-labels--active');

		Animation.handleTransitionEnd(this.getLabelsContainer(), 'max-width').then(() => {
			Dom.removeClass(this.getLabelsContainer(), 'ui-selector-tab-labels--animate-hide');
		});

		Dom.style(this.getLabelsContainer(), 'max-width', null);
	}

	/**
	 * @private
	 */
	handleItemNodeFocus(event: BaseEvent): void
	{
		const { node } = event.getData();
		if (this.focusedNode === node)
		{
			return;
		}

		this.clearNodeFocus();

		this.focusedNode = node;
	}

	/**
	 * @private
	 */
	handleItemNodeUnfocus(): void
	{
		this.clearNodeFocus();
	}

	getAjaxJson(): { [key: string]: any }
	{
		return {
			id: this.getId(),
			context: this.getContext(),
			entities: this.getEntities(),
			preselectedItems: this.getPreselectedItems(),
			recentItemsLimit: this.getRecentItemsLimit(),
			clearUnavailableItems: this.shouldClearUnavailableItems(),
		};
	}
}