97 lines
3 KiB
TypeScript
97 lines
3 KiB
TypeScript
import clsx from 'clsx';
|
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
|
import React, { useCallback, useEffect, useRef } from 'react';
|
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { Text } from 'soapbox/components/ui';
|
|
|
|
const messages = defineMessages({
|
|
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
|
});
|
|
|
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|
|
|
|
|
interface IIconPickerMenu {
|
|
icons: Record<string, Array<string>>
|
|
onClose: () => void
|
|
onPick: (icon: string) => void
|
|
style?: React.CSSProperties
|
|
}
|
|
|
|
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onClose, onPick, style }) => {
|
|
const intl = useIntl();
|
|
|
|
const node = useRef<HTMLDivElement | null>(null);
|
|
|
|
const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
|
|
if (node.current && !node.current.contains(e.target as Node)) {
|
|
onClose();
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.addEventListener('click', handleDocumentClick, false);
|
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
|
|
|
return () => {
|
|
document.removeEventListener('click', handleDocumentClick, false);
|
|
document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any);
|
|
|
|
};
|
|
}, []);
|
|
|
|
const setRef = (c: HTMLDivElement) => {
|
|
node.current = c;
|
|
|
|
if (!c) return;
|
|
|
|
// Nice and dirty hack to display the icons
|
|
c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => {
|
|
const newIcon = document.createElement('span');
|
|
newIcon.innerHTML = `<i class="fa fa-${(elem.parentNode as any).getAttribute('title')} fa-hack"></i>`;
|
|
(elem.parentNode as any).replaceChild(newIcon, elem);
|
|
});
|
|
};
|
|
|
|
const handleClick = (icon: string) => {
|
|
onClose();
|
|
onPick(icon);
|
|
};
|
|
|
|
const renderIcon = (icon: string) => {
|
|
const name = icon.replace('fa fa-', '');
|
|
|
|
return (
|
|
<li key={icon} className='col-span-1 inline-block'>
|
|
<button
|
|
className='flex items-center justify-center rounded-full p-1.5 hover:bg-gray-50 dark:hover:bg-primary-800'
|
|
aria-label={name}
|
|
title={name}
|
|
onClick={() => handleClick(name)}
|
|
>
|
|
<i className={clsx(icon, 'h-[1.375rem] w-[1.375rem] text-lg leading-[1.15]')} />
|
|
</button>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const title = intl.formatMessage(messages.emoji);
|
|
|
|
return (
|
|
<div
|
|
className={clsx('absolute z-[101] -my-0.5')}
|
|
style={{ transform: 'translateX(calc(-1 * env(safe-area-inset-right)))', ...style }}
|
|
ref={setRef}
|
|
>
|
|
<div className='h-[270px] overflow-x-hidden overflow-y-scroll rounded bg-white p-1.5 text-gray-900 dark:bg-primary-900 dark:text-gray-100' aria-label={title}>
|
|
<Text className='px-1.5 py-1'><FormattedMessage id='icon_button.icons' defaultMessage='Icons' /></Text>
|
|
<ul className='grid grid-cols-8'>
|
|
{Object.values(icons).flat().map(icon => renderIcon(icon))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IconPickerMenu;
|