Work on using Emoji Mart picker

icon_picker_admin_config
Sean King 2020-10-16 10:55:35 -06:00
rodzic e882c2a352
commit 3d64f9647c
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 510C52BACD6E7257
9 zmienionych plików z 284 dodań i 116 usunięć

Wyświetl plik

@ -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 => {

Wyświetl plik

@ -0,0 +1,5 @@
import Picker from 'emoji-mart/dist-es/components/picker/picker';
export {
Picker,
};

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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>

Wyświetl plik

@ -0,0 +1,3 @@
export function IconPicker() {
return import(/* webpackChunkName: "icon_picker" */'../components/icon_picker');
}

Wyświetl plik

@ -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);
}
}
}
}

Wyświetl plik

@ -45,7 +45,6 @@
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.3.4",
"@fonticonpicker/react-fonticonpicker": "^1.2.0",
"@popperjs/core": "^2.4.4",
"array-includes": "^3.0.3",
"autoprefixer": "^10.0.0",
@ -114,7 +113,6 @@
"react-swipeable-views": "^0.13.0",
"react-textarea-autosize": "^8.0.0",
"react-toggle": "^4.0.1",
"react-transition-group": "^2.3.0",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0",

Wyświetl plik

@ -1345,11 +1345,6 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@fonticonpicker/react-fonticonpicker@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@fonticonpicker/react-fonticonpicker/-/react-fonticonpicker-1.2.0.tgz#cbcb898f3d788ef50d7090fe623afe2e8e5eb1d7"
integrity sha512-I7U4VNIpwJHnOaWq1v2MSuaUTxPiur/MYG4B1hr+FWI1O7k+tTym1ysdAyfp7YbEdF4Q8TYoY5Q2hbTmhQeTSQ==
"@formatjs/ecma402-abstract@^1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.2.3.tgz#ca94911dd8e9c89eeaabba0f00e2f692979fc27b"
@ -4342,7 +4337,7 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^3.2.1, dom-helpers@^3.3.1, dom-helpers@^3.4.0:
dom-helpers@^3.2.1, dom-helpers@^3.3.1:
version "3.4.0"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8"
integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==
@ -10074,16 +10069,6 @@ react-transition-group@^2.2.1:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react-transition-group@^2.3.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
dependencies:
dom-helpers "^3.4.0"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.4"
react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"