Work on using Emoji Mart picker
This commit is contained in:
@ -4,9 +4,6 @@ require('fork-awesome/css/fork-awesome.css');
|
||||
|
||||
require.context('./images/', true);
|
||||
|
||||
require('@fonticonpicker/react-fonticonpicker/dist/fonticonpicker.base-theme.react.css');
|
||||
require('@fonticonpicker/react-fonticonpicker/dist/fonticonpicker.material-theme.react.css');
|
||||
|
||||
loadPolyfills().then(() => {
|
||||
require('./soapbox/main').default();
|
||||
}).catch(e => {
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
||||
|
||||
export {
|
||||
Picker,
|
||||
};
|
||||
@ -0,0 +1,237 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { IconPicker as IconPickerAsync } from '../utils/async';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import classNames from 'classnames';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import forkAwesomeIcons from '../forkawesome.json';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
|
||||
custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
|
||||
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
||||
});
|
||||
|
||||
let IconPicker; // load asynchronously
|
||||
|
||||
const backgroundImageFn = () => '';
|
||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
||||
|
||||
const categoriesSort = 'custom';
|
||||
|
||||
@injectIntl
|
||||
class IconPickerMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
custom_emojis: PropTypes.object,
|
||||
loading: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onPick: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
loading: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
modifierOpen: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
getI18n = () => {
|
||||
const { intl } = this.props;
|
||||
|
||||
return {
|
||||
search: intl.formatMessage(messages.emoji_search),
|
||||
notfound: intl.formatMessage(messages.emoji_not_found),
|
||||
categories: {
|
||||
search: intl.formatMessage(messages.search_results),
|
||||
custom: intl.formatMessage(messages.custom),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = emoji => {
|
||||
emoji.native = emoji.colons;
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onPick(emoji);
|
||||
}
|
||||
|
||||
buildIcons = (customEmojis, autoplay = false) => {
|
||||
const emojis = [];
|
||||
|
||||
Object.values(customEmojis).forEach(category => {
|
||||
category.forEach(function(icon) {
|
||||
const name = icon.replace('fa fa-', '');
|
||||
emojis.push({
|
||||
id: icon,
|
||||
name,
|
||||
short_names: [name],
|
||||
emoticons: [],
|
||||
keywords: [name],
|
||||
imageUrl: '',
|
||||
custom: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return emojis;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, style, intl, custom_emojis } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ width: 299 }} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { modifierOpen } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('font-icon-picker emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||
<IconPicker
|
||||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
custom={this.buildIcons(custom_emojis)}
|
||||
include={categoriesSort}
|
||||
color=''
|
||||
emoji=''
|
||||
set='twitter'
|
||||
title={title}
|
||||
i18n={this.getI18n()}
|
||||
onClick={this.handleClick}
|
||||
showPreview={false}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
autoFocus
|
||||
emojiTooltip
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class IconPickerDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
||||
intl: PropTypes.object.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
icons: forkAwesomeIcons,
|
||||
active: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.dropdown = c;
|
||||
}
|
||||
|
||||
onShowDropdown = ({ target }) => {
|
||||
this.setState({ active: true });
|
||||
|
||||
if (!IconPicker) {
|
||||
this.setState({ loading: true });
|
||||
|
||||
IconPickerAsync().then(EmojiMart => {
|
||||
IconPicker = EmojiMart.Picker;
|
||||
|
||||
this.setState({ loading: false });
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
}
|
||||
|
||||
onHideDropdown = () => {
|
||||
this.setState({ active: false });
|
||||
}
|
||||
|
||||
onToggle = (e) => {
|
||||
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
|
||||
if (this.state.active) {
|
||||
this.onHideDropdown();
|
||||
} else {
|
||||
this.onShowDropdown(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Escape') {
|
||||
this.onHideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, onPickEmoji, value } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { active, loading, placement, icons } = this.state;
|
||||
|
||||
return (
|
||||
<div className='font-icon-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
||||
<div ref={this.setTargetRef} className='font-icon-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
|
||||
<Icon id={value} />
|
||||
</div>
|
||||
|
||||
<Overlay show={active} placement={placement} target={this.findTarget}>
|
||||
<IconPickerMenu
|
||||
custom_emojis={icons}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { connect } from 'react-redux';
|
||||
import IconPickerDropdown from '../components/icon_picker_dropdown';
|
||||
import { useEmoji } from '../../../actions/emojis';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
frequentlyUsedEmojis: '',
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
|
||||
|
||||
onPickEmoji: emoji => {
|
||||
dispatch(useEmoji(emoji)); // eslint-disable-line react-hooks/rules-of-hooks
|
||||
|
||||
if (onPickEmoji) {
|
||||
onPickEmoji(emoji);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(IconPickerDropdown);
|
||||
@ -26,8 +26,8 @@ import Accordion from '../ui/components/accordion';
|
||||
import SitePreview from './components/site_preview';
|
||||
import ThemeToggle from 'soapbox/features/ui/components/theme_toggle';
|
||||
import { defaultSettings } from 'soapbox/actions/settings';
|
||||
import FontIconPicker from '@fonticonpicker/react-fonticonpicker';
|
||||
import forkAwesomeIcons from './forkawesome.json';
|
||||
import IconPickerDropdown from './components/icon_picker_dropdown';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' },
|
||||
@ -249,9 +249,8 @@ class SoapboxConfig extends ImmutablePureComponent {
|
||||
<div className='row' key={i}>
|
||||
<IconPicker
|
||||
label={intl.formatMessage(messages.promoItemIcon)}
|
||||
placeholder={intl.formatMessage(messages.promoItemIcon)}
|
||||
value={field.get('icon')}
|
||||
onChange={this.handlePromoItemChange(i, 'icon', field, val => val.substring(6))}
|
||||
onChange={this.handlePromoItemChange(i, 'icon', field, val => val.id)}
|
||||
/>
|
||||
<TextInput
|
||||
label={intl.formatMessage(messages.promoItemLabel)}
|
||||
@ -435,25 +434,23 @@ export class IconPicker extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
icons: PropTypes.object,
|
||||
label: FormPropTypes.label,
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
icons: forkAwesomeIcons,
|
||||
placeholder: 'Select icon',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { icons, onChange, value, label, placeholder } = this.props;
|
||||
const { onChange, value, label } = this.props;
|
||||
|
||||
return (
|
||||
<div className='input with_label popup'>
|
||||
<div className='label_input__icon_picker'>
|
||||
<div className='input with_label font_icon_picker'>
|
||||
<div className='label_input__font_icon_picker'>
|
||||
{label && (<label>{label}</label>)}
|
||||
<div className='label_input__wrapper'>
|
||||
<FontIconPicker icons={icons} onChange={onChange} value={value !== '' ? 'fa fa-' + value : value} noSelectedPlaceholder={placeholder} />
|
||||
<div className='label_input_wrapper'>
|
||||
<IconPickerDropdown value={value} onPickEmoji={onChange} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
app/soapbox/features/soapbox_config/utils/async.js
Normal file
3
app/soapbox/features/soapbox_config/utils/async.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function IconPicker() {
|
||||
return import(/* webpackChunkName: "icon_picker" */'../components/icon_picker');
|
||||
}
|
||||
@ -179,6 +179,10 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
.input.font_icon_picker {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.input.with_block_label {
|
||||
max-width: none;
|
||||
|
||||
@ -255,10 +259,6 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
.input.popup {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.input.radio_buttons .radio label {
|
||||
margin-bottom: 5px;
|
||||
font-family: inherit;
|
||||
@ -511,92 +511,18 @@ code {
|
||||
}
|
||||
}
|
||||
|
||||
&__icon_picker {
|
||||
&__font_icon_picker {
|
||||
font-size: 14px;
|
||||
|
||||
.rfipbtn {
|
||||
display: inline-flex;
|
||||
line-height: 19px;
|
||||
min-height: 19px;
|
||||
padding: 0;
|
||||
.font-icon-button {
|
||||
padding: 9px;
|
||||
border: 1px solid var(--highlight-text-color);
|
||||
border-radius: 4px;
|
||||
|
||||
&__icon {
|
||||
margin: 3.5px 0 3.5px 10px;
|
||||
.fa {
|
||||
font-size: 18px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
&--default {
|
||||
.rfipbtn__icon {
|
||||
border-color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.rfipbtn__del {
|
||||
background-color: var(--primary-text-color--faint);
|
||||
color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.rfipbtn__button {
|
||||
border-left: 1px solid var(--highlight-text-color);
|
||||
background-color: var(--foreground-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon--empty {
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
color: var(--primary-text-color--faint);
|
||||
font-size: 16px;
|
||||
line-height: 15px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__current {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
&--default:active,
|
||||
&--default:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border-top-right-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.rfipdropdown {
|
||||
&--default {
|
||||
background-color: var(--background-color);
|
||||
border: none;
|
||||
color: var(--primary-text-color);
|
||||
width: 325px;
|
||||
|
||||
input,
|
||||
select {
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
|
||||
.rfipsearch input:focus,
|
||||
.rfipsearch input:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.rfipicons__ibox,
|
||||
.rfipicons__left,
|
||||
.rfipicons__right {
|
||||
border: none;
|
||||
background-color: var(--foreground-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.rfipicons__cp,
|
||||
.rfipcategory select {
|
||||
border-bottom: 1px solid var(--highlight-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user