MentionsInput.js
21.0 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import React,{PropTypes,Children} from 'react';
import ReactDOM from 'react-dom';
import cx from 'classnames';
import s from './ChatTextarea.scss';
import * as utils from './utils';
import SuggestionsOverlay from './SuggestionsOverlay';
const emptyFunction = function() {};
const KEY = { TAB : 9, RETURN : 13, ESC : 27, UP : 38, DOWN : 40 };
const emojiSheet={
people: ["smile", "laughing", "blush", "smiley", "relaxed", "smirk", "heart_eyes", "kissing_heart", "kissing_closed_eyes", "flushed", "relieved", "satisfied", "grin", "wink", "stuck_out_tongue_winking_eye", "stuck_out_tongue_closed_eyes", "grinning", "kissing", "kissing_smiling_eyes", "stuck_out_tongue", "sleeping", "worried", "frowning", "anguished", "open_mouth", "grimacing", "confused", "hushed", "expressionless", "unamused", "sweat_smile", "sweat", "weary", "pensive", "disappointed", "disappointed_relieved", "confounded", "fearful", "cold_sweat", "persevere", "cry", "sob", "joy", "astonished", "scream", "tired_face", "angry", "rage", "triumph", "sleepy", "yum", "mask", "sunglasses", "dizzy_face", "imp", "smiling_imp", "neutral_face", "no_mouth", "innocent", "alien", "yellow_heart", "blue_heart", "purple_heart", "heart", "green_heart", "broken_heart", "heartbeat", "heartpulse", "two_hearts", "revolving_hearts", "cupid", "sparkling_heart", "sparkles", "star", "star2", "dizzy", "boom", "collision", "anger", "exclamation", "question", "grey_exclamation", "grey_question", "zzz", "dash", "sweat_drops", "notes", "musical_note", "fire", "hankey", "poop", "shit", "+1", "thumbsup", "-1", "thumbsdown", "ok_hand", "punch", "facepunch", "fist", "v", "wave", "hand", "open_hands", "point_up", "point_down", "point_left", "point_right", "raised_hands", "pray", "point_up_2", "clap", "muscle", "metal", "walking", "runner", "running", "couple", "family", "two_men_holding_hands", "two_women_holding_hands", "dancer", "dancers", "ok_woman", "no_good", "information_desk_person", "raised_hand", "bride_with_veil", "person_with_pouting_face", "person_frowning", "bow", "couplekiss", "couple_with_heart", "massage", "haircut", "nail_care", "boy", "girl", "woman", "man", "baby", "older_woman", "older_man", "person_with_blond_hair", "man_with_gua_pi_mao", "man_with_turban", "construction_worker", "cop", "angel", "princess", "smiley_cat", "smile_cat", "heart_eyes_cat", "kissing_cat", "smirk_cat", "scream_cat", "crying_cat_face", "joy_cat", "pouting_cat", "japanese_ogre", "japanese_goblin", "see_no_evil", "hear_no_evil", "speak_no_evil", "guardsman", "skull", "feet", "lips", "kiss", "droplet", "ear", "eyes", "nose", "tongue", "love_letter", "bust_in_silhouette", "busts_in_silhouette", "speech_balloon", "thought_balloon"],
nature: ["sunny", "umbrella", "cloud", "snowflake", "snowman", "zap", "cyclone", "foggy", "ocean", "cat", "dog", "mouse", "hamster", "rabbit", "wolf", "frog", "tiger", "koala", "bear", "pig", "pig_nose", "cow", "boar", "monkey_face", "monkey", "horse", "racehorse", "camel", "sheep", "elephant", "panda_face", "snake", "bird", "baby_chick", "hatched_chick", "hatching_chick", "chicken", "penguin", "turtle", "bug", "honeybee", "ant", "beetle", "snail", "octopus", "tropical_fish", "fish", "whale", "whale2", "dolphin", "cow2", "ram", "rat", "water_buffalo", "tiger2", "rabbit2", "dragon", "goat", "rooster", "dog2", "pig2", "mouse2", "ox", "dragon_face", "blowfish", "crocodile", "dromedary_camel", "leopard", "cat2", "poodle", "paw_prints", "bouquet", "cherry_blossom", "tulip", "four_leaf_clover", "rose", "sunflower", "hibiscus", "maple_leaf", "leaves", "fallen_leaf", "herb", "mushroom", "cactus", "palm_tree", "evergreen_tree", "deciduous_tree", "chestnut", "seedling", "blossom", "ear_of_rice", "shell", "globe_with_meridians", "sun_with_face", "full_moon_with_face", "new_moon_with_face", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "waxing_gibbous_moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "last_quarter_moon_with_face", "first_quarter_moon_with_face", "moon", "earth_africa", "earth_americas", "earth_asia", "volcano", "milky_way", "partly_sunny", "octocat", "squirrel"],
objects: ["bamboo", "gift_heart", "dolls", "school_satchel", "mortar_board", "flags", "fireworks", "sparkler", "wind_chime", "rice_scene", "jack_o_lantern", "ghost", "santa", "christmas_tree", "gift", "bell", "no_bell", "tanabata_tree", "tada", "confetti_ball", "balloon", "crystal_ball", "cd", "dvd", "floppy_disk", "camera", "video_camera", "movie_camera", "computer", "tv", "iphone", "phone", "telephone", "telephone_receiver", "pager", "fax", "minidisc", "vhs", "sound", "speaker", "mute", "loudspeaker", "mega", "hourglass", "hourglass_flowing_sand", "alarm_clock", "watch", "radio", "satellite", "loop", "mag", "mag_right", "unlock", "lock", "lock_with_ink_pen", "closed_lock_with_key", "key", "bulb", "flashlight", "high_brightness", "low_brightness", "electric_plug", "battery", "calling", "email", "mailbox", "postbox", "bath", "bathtub", "shower", "toilet", "wrench", "nut_and_bolt", "hammer", "seat", "moneybag", "yen", "dollar", "pound", "euro", "credit_card", "money_with_wings", "e-mail", "inbox_tray", "outbox_tray", "envelope", "incoming_envelope", "postal_horn", "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "door", "smoking", "bomb", "gun", "hocho", "pill", "syringe", "page_facing_up", "page_with_curl", "bookmark_tabs", "bar_chart", "chart_with_upwards_trend", "chart_with_downwards_trend", "scroll", "clipboard", "calendar", "date", "card_index", "file_folder", "open_file_folder", "scissors", "pushpin", "paperclip", "black_nib", "pencil2", "straight_ruler", "triangular_ruler", "closed_book", "green_book", "blue_book", "orange_book", "notebook", "notebook_with_decorative_cover", "ledger", "books", "bookmark", "name_badge", "microscope", "telescope", "newspaper", "football", "basketball", "soccer", "baseball", "tennis", "8ball", "rugby_football", "bowling", "golf", "mountain_bicyclist", "bicyclist", "horse_racing", "snowboarder", "swimmer", "surfer", "ski", "spades", "hearts", "clubs", "diamonds", "gem", "ring", "trophy", "musical_score", "musical_keyboard", "violin", "space_invader", "video_game", "black_joker", "flower_playing_cards", "game_die", "dart", "mahjong", "clapper", "memo", "pencil", "book", "art", "microphone", "headphones", "trumpet", "saxophone", "guitar", "shoe", "sandal", "high_heel", "lipstick", "boot", "shirt", "tshirt", "necktie", "womans_clothes", "dress", "running_shirt_with_sash", "jeans", "kimono", "bikini", "ribbon", "tophat", "crown", "womans_hat", "mans_shoe", "closed_umbrella", "briefcase", "handbag", "pouch", "purse", "eyeglasses", "fishing_pole_and_fish", "coffee", "tea", "sake", "baby_bottle", "beer", "beers", "cocktail", "tropical_drink", "wine_glass", "fork_and_knife", "pizza", "hamburger", "fries", "poultry_leg", "meat_on_bone", "spaghetti", "curry", "fried_shrimp", "bento", "sushi", "fish_cake", "rice_ball", "rice_cracker", "rice", "ramen", "stew", "oden", "dango", "egg", "bread", "doughnut", "custard", "icecream", "ice_cream", "shaved_ice", "birthday", "cake", "cookie", "chocolate_bar", "candy", "lollipop", "honey_pot", "apple", "green_apple", "tangerine", "lemon", "cherries", "grapes", "watermelon", "strawberry", "peach", "melon", "banana", "pear", "pineapple", "sweet_potato", "eggplant", "tomato", "corn"],
places: ["house", "house_with_garden", "school", "office", "post_office", "hospital", "bank", "convenience_store", "love_hotel", "hotel", "wedding", "church", "department_store", "european_post_office", "city_sunrise", "city_sunset", "japanese_castle", "european_castle", "tent", "factory", "tokyo_tower", "japan", "mount_fuji", "sunrise_over_mountains", "sunrise", "stars", "statue_of_liberty", "bridge_at_night", "carousel_horse", "rainbow", "ferris_wheel", "fountain", "roller_coaster", "ship", "speedboat", "boat", "sailboat", "rowboat", "anchor", "rocket", "airplane", "helicopter", "steam_locomotive", "tram", "mountain_railway", "bike", "aerial_tramway", "suspension_railway", "mountain_cableway", "tractor", "blue_car", "oncoming_automobile", "car", "red_car", "taxi", "oncoming_taxi", "articulated_lorry", "bus", "oncoming_bus", "rotating_light", "police_car", "oncoming_police_car", "fire_engine", "ambulance", "minibus", "truck", "train", "station", "train2", "bullettrain_front", "bullettrain_side", "light_rail", "monorail", "railway_car", "trolleybus", "ticket", "fuelpump", "vertical_traffic_light", "traffic_light", "warning", "construction", "beginner", "atm", "slot_machine", "busstop", "barber", "hotsprings", "checkered_flag", "crossed_flags", "izakaya_lantern", "moyai", "circus_tent", "performing_arts", "round_pushpin", "triangular_flag_on_post", "jp", "kr", "cn", "us", "fr", "es", "it", "ru", "gb", "uk", "de"],
symbols: ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "1234", "zero", "hash", "symbols", "arrow_backward", "arrow_down", "arrow_forward", "arrow_left", "capital_abcd", "abcd", "abc", "arrow_lower_left", "arrow_lower_right", "arrow_right", "arrow_up", "arrow_upper_left", "arrow_upper_right", "arrow_double_down", "arrow_double_up", "arrow_down_small", "arrow_heading_down", "arrow_heading_up", "leftwards_arrow_with_hook", "arrow_right_hook", "left_right_arrow", "arrow_up_down", "arrow_up_small", "arrows_clockwise", "arrows_counterclockwise", "rewind", "fast_forward", "information_source", "ok", "twisted_rightwards_arrows", "repeat", "repeat_one", "new", "top", "up", "cool", "free", "ng", "cinema", "koko", "signal_strength", "u5272", "u5408", "u55b6", "u6307", "u6708", "u6709", "u6e80", "u7121", "u7533", "u7a7a", "u7981", "sa", "restroom", "mens", "womens", "baby_symbol", "no_smoking", "parking", "wheelchair", "metro", "baggage_claim", "accept", "wc", "potable_water", "put_litter_in_its_place", "secret", "congratulations", "m", "passport_control", "left_luggage", "customs", "ideograph_advantage", "cl", "sos", "id", "no_entry_sign", "underage", "no_mobile_phones", "do_not_litter", "non-potable_water", "no_bicycles", "no_pedestrians", "children_crossing", "no_entry", "eight_spoked_asterisk", "eight_pointed_black_star", "heart_decoration", "vs", "vibration_mode", "mobile_phone_off", "chart", "currency_exchange", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "ophiuchus", "six_pointed_star", "negative_squared_cross_mark", "a", "b", "ab", "o2", "diamond_shape_with_a_dot_inside", "recycle", "end", "on", "soon", "clock1", "clock130", "clock10", "clock1030", "clock11", "clock1130", "clock12", "clock1230", "clock2", "clock230", "clock3", "clock330", "clock4", "clock430", "clock5", "clock530", "clock6", "clock630", "clock7", "clock730", "clock8", "clock830", "clock9", "clock930", "heavy_dollar_sign", "copyright", "registered", "tm", "x", "heavy_exclamation_mark", "bangbang", "interrobang", "o", "heavy_multiplication_x", "heavy_plus_sign", "heavy_minus_sign", "heavy_division_sign", "white_flower", "100", "heavy_check_mark", "ballot_box_with_check", "radio_button", "link", "curly_loop", "wavy_dash", "part_alternation_mark", "trident", "black_square", "white_square", "white_check_mark", "black_square_button", "white_square_button", "black_circle", "white_circle", "red_circle", "large_blue_circle", "large_blue_diamond", "large_orange_diamond", "small_blue_diamond", "small_orange_diamond", "small_red_triangle", "small_red_triangle_down", "shipit"]
};
class MentionsInput extends React.Component {
constructor(props) {
super(props);
this.state={
height:null,
minHeight:-Infinity,
maxHeight:Infinity,
selectionStart:null,
suggestions:{},
caretPosition: null,
suggestionsPosition: null,
endIndex:0,
showEmoji:false,
showMention:false,
shiftFocus:0,
fileterEmoji:[],
fileterMention:[]
}
this._rootDOMNode = null;
this._onChange = this._onChange.bind(this);
this._resizeComponent = this._resizeComponent.bind(this);
this._onRootDOMNode = this._onRootDOMNode.bind(this);
this._handleKeyDown=this._handleKeyDown.bind(this);
this._handleKeyUp=this._handleKeyUp.bind(this);
this._filterEmoji=this._filterEmoji.bind(this);
this._shiftFocus=this._shiftFocus.bind(this);
this._clearSuggestions=this._clearSuggestions.bind(this);
this._selectFocused=this._selectFocused.bind(this);
}
static propTypes = {
value:PropTypes.string,
onChange:PropTypes.func,
onKeyDown:PropTypes.func,
onKeyUp:PropTypes.func,
onBlur:PropTypes.func,
onHeightChange:PropTypes.func,
mentions:PropTypes.array,
children:PropTypes.oneOfType([
PropTypes.element,
PropTypes.arrayOf(PropTypes.element)
]),
atEnable:PropTypes.bool
}
static defaultProps={
onChange: emptyFunction,
onKeyDown: emptyFunction,
onBlur: emptyFunction,
onHeightChange: emptyFunction,
atEnable:true
}
componentDidMount(){
this._resizeComponent();
window.addEventListener('resize', this._resizeComponent);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.height !== prevState.height) {
this.props.onHeightChange(this.state.height);
}
}
componentWillUnmount() {
window.removeEventListener('resize', this._resizeComponent);
}
renderSuggestionsOverlay(){
// if(!utils.isNumber(this.state.selectionStart)) {
// // do not show suggestions when the input does not have the focus
// return null;
// }
const {fileterEmoji,fileterMention,shiftFocus}=this.state,self=this;
const {atEnable}=this.props;
if(fileterEmoji.length<1&&fileterMention.length<1){
return null
}else if(fileterEmoji.length>0){
return (
<div className={cx(s.suggestion_overlay)} ref='suggestion_overlay'>
{ fileterEmoji.map((emoji,i)=>{
const url=`/img/emoji/${emoji}.png`;
return (
<div key={i} onClick={self.choseEmoji.bind(self,i)} className={cx(s.banner,s.contact_name,s.item,s.line,i==shiftFocus?s.active:'')}>
<span>
<img className={cx(s.is_leading,s.avatar,s.img_circle,s.img_24)} src={url} />
</span>
<span className={cx(s.name,s.text_overflow)}>:{emoji}:</span>
</div>
)
})
}
</div>
)
}else if(atEnable&&fileterMention.length>0){
return (
<div className={cx(s.suggestion_overlay)} ref='suggestion_overlay'>
{
fileterMention.map((mention,i)=>{
return(
<div key={i} onClick={self.choseMention.bind(self,i)} className={cx(s.banner,s.contact_name,s.item,s.line,i==shiftFocus?s.active:'')}>
<img className={cx(s.is_leading,s.avatar,s.img_circle,s.img_24)} src="/img/user.jpg" />
<span className={cx(s.name,s.text_overflow)}>{mention.name}</span>
</div>
)
})
}
</div>
)
}
}
render(){
let {value,...props} = this.props;
props = {...props};
props.style = {
...props.style,
height: this.state.height
};
let maxHeight = Math.max(
props.style.maxHeight ? props.style.maxHeight : Infinity,
this.state.maxHeight);
if (maxHeight < this.state.height) {
props.style.overflow = 'hidden';
}
return(
<div >
<textarea
style={props.style}
value={this.props.value}
onChange={this._onChange}
onKeyUp={this._handleKeyUp}
onKeyDown={this._handleKeyDown}
ref={this._onRootDOMNode} />
{ this.renderSuggestionsOverlay() }
</div>
)
}
choseMention(index,e){
const {showEmoji,showMention,fileterEmoji,fileterMention,endIndex}=this.state;
const {onChange,value}=this.props;
if(showMention){
const backValue=utils.mentionReplace(value,endIndex,fileterMention[index].name)
onChange(backValue);
}
this._clearSuggestions();
}
choseEmoji(index,e){
const {showEmoji,showMention,fileterEmoji,fileterMention,endIndex}=this.state;
const {onChange,value}=this.props;
if(showEmoji){
const backValue=utils.emojiReplace(value,endIndex,fileterEmoji[index])
onChange(backValue);
}
this._clearSuggestions();
}
_filterEmoji(searchKey){
const fileters=utils.fileterEmoji(searchKey,emojiSheet);
this.setState({
fileterEmoji:fileters,
showEmoji:true,
fileterMention:[],
showMention:false
});
return fileters.length>0;
}
_filterMention(searchKey){
const {mentions,atEnable}=this.props;
const fileters=atEnable?utils.fileterMention(searchKey,mentions):[];
this.setState({
fileterEmoji:[],
showEmoji:false,
fileterMention:fileters,
showMention:true
});
return fileters.length>0;
}
_shiftFocus(delta){
const {shiftFocus,fileterEmoji,showEmoji,fileterMention,showMention}=this.state;
let max=0;
if(showEmoji){
max=fileterEmoji.length;
}else if(showMention){
max=fileterMention.length;
}
this.setState({
shiftFocus:(max+shiftFocus+delta)%max
})
}
_selectFocused(){
const {showEmoji,showMention,shiftFocus,fileterEmoji,fileterMention,endIndex}=this.state;
const {onChange,value}=this.props;
if(showEmoji){
const backValue=utils.emojiReplace(value,endIndex,fileterEmoji[shiftFocus])
onChange(backValue);
}
if(showMention){
const backValue=utils.mentionReplace(value,endIndex,fileterMention[shiftFocus].name)
onChange(backValue);
}
this._clearSuggestions();
}
_clearSuggestions(){
this.setState({
fileterEmoji:[],
fileterMention:[],
shiftFocus:0,
showEmoji:false,
showMention:false
});
this._rootDOMNode.focus();
}
_handleKeyUp(e){
const value=e.target.value;
const {mentions,atEnable}=this.props;
const {fileterEmoji,fileterMention}=this.state;
const {filterString,trigger}=utils.substringKeyword(e.target.value,e.target.selectionEnd);
this.setState({
endIndex:e.target.selectionEnd
});
if(e.shiftKey&&e.keyCode==13){
e.preventDefault();
return;
}
if(filterString.length>0){
let noSendFlag=false;
if('mention'==trigger){
noSendFlag=this._filterMention(filterString.substring(1));
}
if('emoji'==trigger){
noSendFlag=this._filterEmoji(filterString.substring(1));
}
if(noSendFlag){
e.preventDefault();
switch(e.keyCode) {
case KEY.ESC: {
this._clearSuggestions();
return;
}
case KEY.DOWN: {
this._shiftFocus(+1);
return;
}
case KEY.UP: {
this._shiftFocus(-1);
return;
}
case KEY.RETURN: {
this._selectFocused();
return;
}
case KEY.TAB: {
return;
}
}
return ;
}
}else{
this._clearSuggestions();
}
if(atEnable&&e.keyCode==13){
const backMention=utils.backMention(mentions,e.target.value);
this.props.onKeyUp(e,backMention);
return;
}
this.props.onKeyUp(e);
return;
}
_handleKeyDown(e){
if(e.shiftKey&&e.keyCode==13){
return;
}else if(e.keyCode==KEY.RETURN||e.keyCode==KEY.UP||e.keyCode==KEY.DOWN){
e.preventDefault();
}
return;
}
_resizeComponent() {
this.setState(utils.calculateNodeHeight(this._rootDOMNode));
}
_onRootDOMNode(node) {
this._rootDOMNode = node;
}
_onChange(e) {
this._resizeComponent();
this.props.onChange(e);
}
}
export default MentionsInput;