Your IP : 3.145.110.193


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/ui/reactions-select/src/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/ui/reactions-select/src/reactions-select.js

import {Type, Dom, Tag, Browser, Loc, Event} from 'main.core';
import {EventEmitter} from 'main.core.events';
import {Lottie} from "ui.lottie";
import {Popup} from 'main.popup';

import "./reactions-select.css";
import "./reactions-icon.css";

import likeAnimatedEmojiData from '../animations/em_01.json';
import laughAnimatedEmojiData from '../animations/em_02.json';
import wonderAnimatedEmojiData from '../animations/em_03.json';
import cryAnimatedEmojiData from '../animations/em_04.json';
import angryAnimatedEmojiData from '../animations/em_05.json';
import facepalmAnimatedEmojiData from '../animations/em_06.json';
import admireAnimatedEmojiData from '../animations/em_07.json';

export const reactionType = Object.freeze({
	like: 'like',
	kiss: 'kiss',
	laugh: 'laugh',
	wonder: 'wonder',
	cry: 'cry',
	angry: 'angry',
	facepalm: 'facepalm',
});

export const reactionLottieAnimations = Object.freeze({
	like: likeAnimatedEmojiData,
	laugh: laughAnimatedEmojiData,
	wonder: wonderAnimatedEmojiData,
	cry: cryAnimatedEmojiData,
	angry: angryAnimatedEmojiData,
	facepalm: facepalmAnimatedEmojiData,
	kiss: admireAnimatedEmojiData,
});

export const reactionCssClass = Object.freeze({
	like: "reaction-icon_like",
	laugh: "reaction-icon_laugh",
	wonder: "reaction-icon_wonder",
	cry: "reaction-icon_cry",
	angry: "reaction-icon_angry",
	facepalm: "reaction-icon_facepalm",
	kiss: "reaction-icon_kiss",
});

export const reactionSelectEvents = Object.freeze({
	show: 'show',
	hide: 'hide',
	mouseenter: 'mouseenter',
	mouseleave: 'mouseleave',
	select: 'select',
	touchenter: 'touchenter',
	touchleave: 'touchleave',
	touchend: 'touchend',
	touchmove: 'touchmove',
});

type TouchEventHandler = (e: TouchEvent) => void;

type ReactionsSelectForcePosition = {
	left: number;
	top: number;
}

type ReactionsSelectPosition = | HTMLElement | ReactionsSelectForcePosition;
type ReactionsSelectOptions = {
	name?: string;
	position: ReactionsSelectPosition;
	containerClassname?: string;
}

/*
* Emitted events
* show,
* hide,
* select,
* mouseleave,
* mouseenter,
* touchenter,
* touchleave,
* touchend
*/

export class ReactionsSelect extends EventEmitter
{
	#name: string;
	#containerClassname: string = '';
	#position: ReactionsSelectPosition | null;
	#baseClassname: string;
	#popupContentClassname: string;
	#reactionsPopup: Popup | null;
	#popupContent: HTMLElement | null = null;
	#availableReactions: string[];
	#hoveredElement: HTMLElement | null;
	#touchMoveHandler: TouchEventHandler | null = null;
	#touchEndHandler: TouchEventHandler | null = null;
	#mouseEnterHandler: MouseEvent | null = null;
	#mouseLeaveHandler: MouseEvent | null = null;
	#isPopupTouched: boolean = false;
	#showClassname: null;
	#hideClassname: null;

	constructor(options: ReactionsSelectOptions = {name: 'ReactionsSelect'})
	{
		super();
		this.setEventNamespace('UI:ReactionsSelect');

		this.#name = Type.isString(options.name) ? options.name : this.#generateName();
		this.#baseClassname = 'reaction-select';
		this.#popupContentClassname = `${this.#baseClassname}_container`;
		this.#availableReactions = Object.keys(reactionType);
		this.#reactionsPopup = null;
		this.#containerClassname = Type.isString(options.containerClassname) ? options.containerClassname : '';
		this.#position = this.#checkPositionOption(options.position) ? options.position : null;
		this.#hoveredElement = null;
		this.#showClassname = 'reactions-popup-show';
		this.#hideClassname = 'reactions-popup-close';

		if (Browser.isMobile())
		{
			this.#touchMoveHandler = this.#handleTouchMove.bind(this);
			this.#touchEndHandler = this.#handleTouchEnd.bind(this);
		}
		else
		{
			this.#mouseEnterHandler = this.#handleMouseEnter.bind(this);
			this.#mouseLeaveHandler = this.#handleMouseLeave.bind(this);
			this.#touchMoveHandler = null;
			this.#touchEndHandler = null;
		}
	}

	static Events = reactionSelectEvents;
	static getLottieAnimation(reactionName?: string): Object | null
	{
		if (!reactionName)
		{
			return reactionLottieAnimations;
		}

		return reactionLottieAnimations[reactionName] || null;
	}

	static getReactionCssClass(reactionName?: string): Object | null
	{
		if (!reactionName)
		{
			return reactionCssClass;
		}

		return reactionCssClass[reactionName] || null;
	}

	show(): void
	{
		if (!this.#reactionsPopup)
		{
			this.#createReactionsPopup();
		}

		if (Browser.isMobile())
		{
			this.#disableScrollOnMobile();
			Event.bind(window, 'touchmove', this.#touchMoveHandler);
			Event.bind(window, 'touchend', this.#touchEndHandler);
		}

		this.#reactionsPopup.show();

		this.emit(ReactionsSelect.Events.show);
	}

	hide(): void
	{
		if (this.#reactionsPopup)
		{
			Event.unbind(this.#popupContent, 'mouseleave', this.#mouseLeaveHandler);
			Event.unbind(this.#popupContent, 'mouseenter', this.#mouseEnterHandler);
			Event.unbind(window, 'touchmove', this.#touchMoveHandler);
			Event.unbind(window, 'touchend', this.#touchEndHandler);

			this.#reactionsPopup.close();
			this.#reactionsPopup = null;
			this.#popupContent = null;

			this.#enableScrollOnMobile();
		}

		this.emit(ReactionsSelect.Events.hide);
	}

	isShown(): boolean
	{
		return this.#reactionsPopup && this.#reactionsPopup.isShown();
	}

	getName(): string
	{
		return this.#name;
	}

	#createReactionsPopup()
	{
		this.#reactionsPopup = new Popup({
			id: 'reactions-list-'+this.#name,
			content: this.#renderPopupContent(),
			...this.#getPopupPositionOptions(),
			noAllPaddings: true,
			borderRadius: '25px',
			animation: {
				showClassName: this.#showClassname,
				closeClassName: this.#hideClassname,
				closeAnimationType: 'animation',
			},
			cacheable: false,
			disableScroll: Browser.isMobile(),
			className: 'reaction-select-popup',
		});
	}

	#renderPopupContent(): HTMLElement
	{
		this.#popupContent = Tag.render`
			<div class="${this.#getPopupContentClassname()}">
				${this.#renderReactionsList()}
			</div>
		`;

		if (!Browser.isMobile())
		{
			Event.bind(this.#popupContent, 'mouseleave', this.#mouseLeaveHandler);
			Event.bind(this.#popupContent, 'mouseenter', this.#mouseEnterHandler);
		}

		return this.#popupContent;
	}

	#getPopupContentClassname(): string
	{
		const baseClassname = `${this.#popupContentClassname}`;
		const mobileDeviceModifier = `${Browser.isMobile() ? '--mobile' : ''}`;

		return [
			baseClassname,
			this.#containerClassname,
			mobileDeviceModifier
		].join(' ');
	}

	#renderReactionsList(): HTMLElement
	{
		const container: HTMLElement = Tag.render`<div class="${this.#baseClassname}_list"></div>`;

		this.#availableReactions.forEach((reactionName) => {
			Dom.append(this.#renderReactionItem(reactionName), container);
		});

		return container;
	}

	#renderReactionItem(reactionName: string): HTMLElement
	{
		const className = `${this.#baseClassname}_reaction-icon-item`;
		const reactionTitle = Loc.getMessage(`REACTIONS_SELECT_${reactionName.toUpperCase()}`);
		const reactionIcon = this.#renderAnimatedReactionIcon(reactionName);
		const reactionHoverArea = this.#renderReactionItemHoverArea(reactionName);

		return Tag.render`
			<div
				class="${className}"
				data-reaction="${reactionName}"
				title="${reactionTitle}"
			>
				${reactionHoverArea}
				${reactionIcon}
			</div>
		`;
	}

	#renderAnimatedReactionIcon(reactionName: string): HTMLElement
	{
		const reactionIcon = Tag.render`<div class="${this.#baseClassname}_reaction-icon"></div>`;

		Lottie.loadAnimation({
			renderer: 'svg',
			container: reactionIcon,
			animationData: reactionLottieAnimations[reactionName],
		});

		return reactionIcon;
	}

	#renderReactionItemHoverArea(reactionName: string): HTMLElement
	{
		const className = `${this.#baseClassname}_reaction-hover-area`;
		const reactionHoverArea: HTMLElement= Tag.render`<div class="${className}"></div>`;

		if (!Browser.isMobile())
		{
			Event.bind(reactionHoverArea, 'click', () => {
				this.emit(ReactionsSelect.Events.select, {
					reaction: reactionName,
				});
			});
		}

		return reactionHoverArea;
	}

	#getPopupPositionForBindElement()
	{
		const leftShift = -50;
		const topShift = -60;

		const {left = 0, top = 0} = Dom.getPosition(this.#position);

		return {left: left + leftShift, top: top + topShift};
	}

	#getPopupPositionOptions(): Object
	{
		if (Type.isPlainObject(this.#position) && this.#position?.left && this.#position?.top)
		{
			return {
				bindElement: this.#position,
			};
		}
		else if (Type.isDomNode(this.#position))
		{
			return {
				bindElement: this.#getPopupPositionForBindElement(),
			};
		}

		return {};
	}

	#handleTouchMove(e: TouchEvent): void
	{
		const reactionHoverArea = this.#getReactionHoverAreaFromTouch(e);
		const isCurrentTouchOnPopup = this.#checkIsPopupTouched(e);

		if (this.#isPopupTouched === false && isCurrentTouchOnPopup === true)
		{
			this.emit( ReactionsSelect.Events.touchenter);
		}
		else if (this.#isPopupTouched === true && isCurrentTouchOnPopup === false)
		{
			this.emit(ReactionsSelect.Events.mouseleave);
		}

		this.#isPopupTouched = isCurrentTouchOnPopup;

		if (reactionHoverArea === null)
		{
			Dom.removeClass(this.#hoveredElement, '--hover');
			this.#hoveredElement = null;
		}
		else if (this.#hoveredElement !== reactionHoverArea)
		{
			Dom.removeClass(this.#hoveredElement, '--hover');
			this.#hoveredElement = reactionHoverArea;
			Dom.addClass(this.#hoveredElement, '--hover');
		}

		this.emit(ReactionsSelect.Events.touchmove);
	}

	#handleTouchEnd(e: TouchEvent): void
	{
		const reactionHoverArea = this.#hoveredElement || this.#getReactionHoverAreaFromTouch(e);

		const reactionName = reactionHoverArea?.parentElement.getAttribute('data-reaction');

		if (reactionName)
		{
			this.emit(ReactionsSelect.Events.select, {
				reaction: reactionName || null,
			});
		}
		this.emit(ReactionsSelect.Events.touchend);
	}

	#handleMouseLeave(e: MouseEvent): void
	{
		this.emit(ReactionsSelect.Events.mouseleave, e);
	}

	#handleMouseEnter(e: MouseEvent): void
	{
		this.emit(ReactionsSelect.Events.mouseenter, e);
	}

	#getReactionHoverAreaFromTouch(e: TouchEvent): HTMLElement | null
	{
		const element = this.#getElementFromTouchEvent(e);

		return this.#isReactionHoverArea(element) ? element : null;
	}

	#isReactionHoverArea(element: Element | null): boolean
	{
		return element && element.classList.contains(`reaction-select_reaction-hover-area`);
	}

	#touchMoveScrollListener(e)
	{
		e.preventDefault();
	}

	#disableScrollOnMobile(): void
	{
		if (!Browser.isMobile())
		{
			return;
		}

		if (app)
		{
			app.exec('disableTabScrolling');
		}
		this.emit('onPullDownDisable');
		Event.bind(window, 'touchmove', this.#touchMoveScrollListener, { passive: false });
	}

	#enableScrollOnMobile(): void
	{
		if (!Browser.isMobile())
		{
			return;
		}
		document.removeEventListener('touchmove', this.#touchMoveScrollListener, { passive: false });
		this.emit('onPullDownEnable');
	}

	#generateName(): string
	{
		const num = Math.round(Math.random() * 1000);

		return `ReactionsSelect${num}`;
	}

	#checkPositionOption(position: ReactionsSelectPosition): boolean
	{
		if (position === undefined)
		{
			console.warn('UI.ReactionSelect: "position" parameter is required');
			return false;
		}
		else if (!Type.isDomNode(position) && !Type.isPlainObject(position))
		{
			console.warn('UI.ReactionSelect: "position" parameter must be an Object or an HTMLElement');
			return false;
		}
		else if (
			!Type.isPlainObject(position)
			&& !Type.isDomNode(position)
		)
		{
			console.warn('UI.ReactionSelect: "position" must be HTMLElement');
			return false;
		}
		else if (
			Type.isPlainObject(position) && !Type.isNumber(position.left)
		)
		{
			console.warn('UI.ReactionSelect: position.left must be a number');
			return false;
		}
		else if (Type.isPlainObject(position) && !Type.isNumber(position.top))
		{
			console.warn('UI.ReactionSelect: position.top must be a number');
			return false;
		}

		return true;
	}

	#getElementFromTouchEvent(e: TouchEvent): HTMLElement | null
	{
		if (!e || !e.touches || e.touches.length < 1)
		{
			return null;
		}
		const touchX = e.touches.item(0)?.pageX;
		const touchY = e.touches.item(0)?.pageY;

		return document.elementFromPoint(touchX, touchY);
	}

	#checkIsPopupTouched(e: TouchEvent): boolean
	{
		const element = this.#getElementFromTouchEvent(e);

		return Boolean(element.closest(`.${this.#popupContentClassname}`));
	}
}