]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js
Add emoji autosuggest (#5053)
[mastodon.git] / app / javascript / mastodon / features / compose / components / emoji_picker_dropdown.js
1 import React from 'react';
2 import PropTypes from 'prop-types';
3 import { defineMessages, injectIntl } from 'react-intl';
4 import { Picker, Emoji } from 'emoji-mart';
5 import { Overlay } from 'react-overlays';
6 import classNames from 'classnames';
7 import ImmutablePropTypes from 'react-immutable-proptypes';
8
9 const messages = defineMessages({
10 emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
11 emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
12 emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
13 custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
14 recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
15 search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
16 people: { id: 'emoji_button.people', defaultMessage: 'People' },
17 nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
18 food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
19 activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
20 travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
21 objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
22 symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
23 flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
24 });
25
26 const assetHost = process.env.CDN_HOST || '';
27
28 const backgroundImageFn = () => `${assetHost}/emoji/sheet.png`;
29
30 class ModifierPickerMenu extends React.PureComponent {
31
32 static propTypes = {
33 active: PropTypes.bool,
34 onSelect: PropTypes.func.isRequired,
35 onClose: PropTypes.func.isRequired,
36 };
37
38 handleClick = (e) => {
39 const modifier = [].slice.call(e.currentTarget.parentNode.children).indexOf(e.target) + 1;
40 this.props.onSelect(modifier);
41 }
42
43 componentWillReceiveProps (nextProps) {
44 if (nextProps.active) {
45 this.attachListeners();
46 } else {
47 this.removeListeners();
48 }
49 }
50
51 componentWillUnmount () {
52 this.removeListeners();
53 }
54
55 handleDocumentClick = e => {
56 if (this.node && !this.node.contains(e.target)) {
57 this.props.onClose();
58 }
59 }
60
61 attachListeners () {
62 document.addEventListener('click', this.handleDocumentClick, false);
63 document.addEventListener('touchend', this.handleDocumentClick, false);
64 }
65
66 removeListeners () {
67 document.removeEventListener('click', this.handleDocumentClick, false);
68 document.removeEventListener('touchend', this.handleDocumentClick, false);
69 }
70
71 setRef = c => {
72 this.node = c;
73 }
74
75 render () {
76 const { active } = this.props;
77
78 return (
79 <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
80 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button>
81 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button>
82 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button>
83 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button>
84 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button>
85 <button onClick={this.handleClick}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button>
86 </div>
87 );
88 }
89
90 }
91
92 class ModifierPicker extends React.PureComponent {
93
94 static propTypes = {
95 active: PropTypes.bool,
96 modifier: PropTypes.number,
97 onChange: PropTypes.func,
98 onClose: PropTypes.func,
99 onOpen: PropTypes.func,
100 };
101
102 handleClick = () => {
103 if (this.props.active) {
104 this.props.onClose();
105 } else {
106 this.props.onOpen();
107 }
108 }
109
110 handleSelect = modifier => {
111 this.props.onChange(modifier);
112 this.props.onClose();
113 }
114
115 render () {
116 const { active, modifier } = this.props;
117
118 return (
119 <div className='emoji-picker-dropdown__modifiers'>
120 <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} />
121 <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
122 </div>
123 );
124 }
125
126 }
127
128 @injectIntl
129 class EmojiPickerMenu extends React.PureComponent {
130
131 static propTypes = {
132 custom_emojis: ImmutablePropTypes.list,
133 onClose: PropTypes.func.isRequired,
134 onPick: PropTypes.func.isRequired,
135 style: PropTypes.object,
136 placement: PropTypes.string,
137 arrowOffsetLeft: PropTypes.string,
138 arrowOffsetTop: PropTypes.string,
139 intl: PropTypes.object.isRequired,
140 };
141
142 static defaultProps = {
143 style: {},
144 placement: 'bottom',
145 };
146
147 state = {
148 modifierOpen: false,
149 modifier: 1,
150 };
151
152 handleDocumentClick = e => {
153 if (this.node && !this.node.contains(e.target)) {
154 this.props.onClose();
155 }
156 }
157
158 componentDidMount () {
159 document.addEventListener('click', this.handleDocumentClick, false);
160 document.addEventListener('touchend', this.handleDocumentClick, false);
161 }
162
163 componentWillUnmount () {
164 document.removeEventListener('click', this.handleDocumentClick, false);
165 document.removeEventListener('touchend', this.handleDocumentClick, false);
166 }
167
168 setRef = c => {
169 this.node = c;
170 }
171
172 getI18n = () => {
173 const { intl } = this.props;
174
175 return {
176 search: intl.formatMessage(messages.emoji_search),
177 notfound: intl.formatMessage(messages.emoji_not_found),
178 categories: {
179 search: intl.formatMessage(messages.search_results),
180 recent: intl.formatMessage(messages.recent),
181 people: intl.formatMessage(messages.people),
182 nature: intl.formatMessage(messages.nature),
183 foods: intl.formatMessage(messages.food),
184 activity: intl.formatMessage(messages.activity),
185 places: intl.formatMessage(messages.travel),
186 objects: intl.formatMessage(messages.objects),
187 symbols: intl.formatMessage(messages.symbols),
188 flags: intl.formatMessage(messages.flags),
189 custom: intl.formatMessage(messages.custom),
190 },
191 };
192 }
193
194 handleClick = emoji => {
195 if (!emoji.native) {
196 emoji.native = emoji.colons;
197 }
198
199 this.props.onClose();
200 this.props.onPick(emoji);
201 }
202
203 handleModifierOpen = () => {
204 this.setState({ modifierOpen: true });
205 }
206
207 handleModifierClose = () => {
208 this.setState({ modifierOpen: false });
209 }
210
211 handleModifierChange = modifier => {
212 if (modifier !== this.state.modifier) {
213 this.setState({ modifier });
214 }
215 }
216
217 render () {
218 const { style, intl } = this.props;
219 const title = intl.formatMessage(messages.emoji);
220 const { modifierOpen, modifier } = this.state;
221
222 return (
223 <div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
224 <Picker
225 perLine={8}
226 emojiSize={22}
227 sheetSize={32}
228 color=''
229 emoji=''
230 set='twitter'
231 title={title}
232 i18n={this.getI18n()}
233 onClick={this.handleClick}
234 skin={modifier}
235 backgroundImageFn={backgroundImageFn}
236 />
237
238 <ModifierPicker
239 active={modifierOpen}
240 modifier={modifier}
241 onOpen={this.handleModifierOpen}
242 onClose={this.handleModifierClose}
243 onChange={this.handleModifierChange}
244 />
245 </div>
246 );
247 }
248
249 }
250
251 @injectIntl
252 export default class EmojiPickerDropdown extends React.PureComponent {
253
254 static propTypes = {
255 custom_emojis: ImmutablePropTypes.list,
256 intl: PropTypes.object.isRequired,
257 onPickEmoji: PropTypes.func.isRequired,
258 };
259
260 state = {
261 active: false,
262 };
263
264 setRef = (c) => {
265 this.dropdown = c;
266 }
267
268 onShowDropdown = () => {
269 this.setState({ active: true });
270 }
271
272 onHideDropdown = () => {
273 this.setState({ active: false });
274 }
275
276 onToggle = (e) => {
277 if (!e.key || e.key === 'Enter') {
278 if (this.state.active) {
279 this.onHideDropdown();
280 } else {
281 this.onShowDropdown();
282 }
283 }
284 }
285
286 handleKeyDown = e => {
287 if (e.key === 'Escape') {
288 this.onHideDropdown();
289 }
290 }
291
292 setTargetRef = c => {
293 this.target = c;
294 }
295
296 findTarget = () => {
297 return this.target;
298 }
299
300 render () {
301 const { intl, onPickEmoji } = this.props;
302 const title = intl.formatMessage(messages.emoji);
303 const { active } = this.state;
304
305 return (
306 <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
307 <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
308 <img
309 className='emojione'
310 alt='🙂'
311 src={`${assetHost}/emoji/1f602.svg`}
312 />
313 </div>
314
315 <Overlay show={active} placement='bottom' target={this.findTarget}>
316 <EmojiPickerMenu
317 custom_emojis={this.props.custom_emojis}
318 onClose={this.onHideDropdown}
319 onPick={onPickEmoji}
320 />
321 </Overlay>
322 </div>
323 );
324 }
325
326 }
This page took 0.16748 seconds and 4 git commands to generate.