├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── css └── emoji-mart.css ├── data ├── all.json ├── apple.json ├── emojione.json ├── facebook.json ├── google.json ├── messenger.json └── twitter.json ├── docs ├── app.vue ├── bundle.js ├── images │ └── parrot.gif ├── index.html ├── index.js └── webpack.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── scripts ├── build-data.js ├── build.js └── define.js ├── spec ├── emoji-index-spec.js ├── picker-spec.js └── webpack.config.js ├── src ├── components │ ├── anchors.old.js │ ├── anchors.vue │ ├── category.old.js │ ├── category.vue │ ├── emoji │ │ ├── emoji.js │ │ ├── emoji.vue │ │ ├── nimble-emoji.js │ │ └── nimbleEmoji.vue │ ├── index.js │ ├── picker │ │ ├── nimble-picker.js │ │ ├── nimblePicker.vue │ │ ├── picker.js │ │ └── picker.vue │ ├── preview.old.js │ ├── preview.vue │ ├── search.old.js │ ├── search.vue │ ├── skins.old.js │ └── skins.vue ├── index.js ├── polyfills │ ├── createClass.js │ ├── extends.js │ ├── inherits.js │ ├── objectGetPrototypeOf.js │ ├── possibleConstructorReturn.js │ └── stringFromCodePoint.js ├── svgs │ └── index.js ├── utils │ ├── data.js │ ├── emoji-index │ │ ├── emoji-index.js │ │ └── nimble-emoji-index.js │ ├── frequently.js │ ├── index.js │ ├── shared-props.js │ └── store.js ├── vendor │ └── raf-polyfill.js └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": [ 4 | "check-es2015-constants", 5 | "transform-es2015-arrow-functions", 6 | "transform-es2015-block-scoped-functions", 7 | "transform-es2015-block-scoping", 8 | "transform-es2015-classes", 9 | "transform-es2015-computed-properties", 10 | "transform-es2015-destructuring", 11 | "transform-es2015-duplicate-keys", 12 | "transform-es2015-for-of", 13 | "transform-es2015-function-name", 14 | "transform-es2015-literals", 15 | "transform-es2015-object-super", 16 | "transform-es2015-parameters", 17 | "transform-es2015-shorthand-properties", 18 | "transform-es2015-spread", 19 | "transform-es2015-sticky-regex", 20 | "transform-es2015-template-literals", 21 | "transform-es2015-unicode-regex", 22 | "transform-regenerator", 23 | 24 | "transform-object-rest-spread", 25 | "transform-runtime", 26 | [ 27 | "transform-define", "scripts/define.js" 28 | ], 29 | [ 30 | "module-resolver", 31 | { 32 | "alias": { 33 | "babel-runtime/core-js/object/get-prototype-of": "./src/polyfills/objectGetPrototypeOf", 34 | "babel-runtime/helpers/extends": "./src/polyfills/extends", 35 | "babel-runtime/helpers/inherits": "./src/polyfills/inherits", 36 | "babel-runtime/helpers/createClass": "./src/polyfills/createClass", 37 | "babel-runtime/helpers/possibleConstructorReturn": "./src/polyfills/possibleConstructorReturn" 38 | } 39 | } 40 | ] 41 | ], 42 | "env": { 43 | "cjs": { 44 | "plugins": [ 45 | "transform-es2015-modules-commonjs" 46 | ] 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | dist-es/ 4 | stats.json 5 | report.html 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dist/report.html 2 | scripts/ 3 | .* 4 | 5 | src/ 6 | docs/ 7 | spec/ 8 | example/ 9 | karma.conf.js 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Missive 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This project has been forked from [emoji-mart](https://www.npmjs.com/package/emoji-mart) which was written for React 2 | 3 |
4 |
Emoji Mart (Vue) is a Slack-like customizable
emoji picker component for VueJS 5 |
DemoChangelog 6 |
7 |
8 | 9 | ## Installation 10 | 11 | `npm install --save emoji-mart-vue` 12 | 13 | ## Components 14 | ### Picker 15 | ```js 16 | import { Picker } from 'emoji-mart-vue' 17 | ``` 18 | 19 | ```html 20 | 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 | | Prop | Required | Default | Description | 28 | | ---- | :------: | ------- | ----------- | 29 | | **autoFocus** | | `false` | Auto focus the search input when mounted | 30 | | **color** | | `#ae65c5` | The top bar anchors select and hover color | 31 | | **emoji** | | `department_store` | The emoji shown when no emojis are hovered, set to an empty string to show nothing | 32 | | **include** | | `[]` | Only load included categories. Accepts [I18n categories keys](#i18n). Order will be respected, except for the `recent` category which will always be the first. | 33 | | **exclude** | | `[]` | Don't load excluded categories. Accepts [I18n categories keys](#i18n). | 34 | | **custom** | | `[]` | [Custom emojis](#custom-emojis) | 35 | | **recent** | | | Pass your own frequently used emojis as array of string IDs | 36 | | **emojiSize** | | `24` | The emoji width and height | 37 | | **perLine** | | `9` | Number of emojis per line. While there’s no minimum or maximum, this will affect the picker’s width. This will set *Frequently Used* length as well (`perLine * 4`) | 38 | | **i18n** | | [`{…}`](#i18n) | [An object](#i18n) containing localized strings | 39 | | **native** | | `false` | Renders the native unicode emoji | 40 | | **set** | | `apple` | The emoji set: `'apple', 'google', 'twitter', 'emojione', 'messenger', 'facebook'` | 41 | | **sheetSize** | | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | 42 | | **backgroundImageFn** | | ```((set, sheetSize) => …)``` | A Fn that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | 43 | | **emojisToShowFilter** | | ```((emoji) => true)``` | A Fn to choose whether an emoji should be displayed or not | 44 | | **showPreview** | | `true` | Display preview section | 45 | | **showSearch** | | `true` | Display search section | 46 | | **showCategories** | | `true` | Display categories | 47 | | **showSkinTones** | | `true` | Display skin tones picker | 48 | | **emojiTooltip** | | `false` | Show emojis short name when hovering (title) | 49 | | **skin** | | | Forces skin color: `1, 2, 3, 4, 5, 6` | 50 | | **defaultSkin** | | `1` | Default skin color: `1, 2, 3, 4, 5, 6` | 51 | | **pickerStyles** | | | Inline styles applied to the root element. Useful for positioning | 52 | | **title** | | `Emoji Mart™` | The title shown when no emojis are hovered | 53 | | **infiniteScroll** | | `true` | Scroll continuously through the categories | 54 | 55 | 56 | | Event | Description | 57 | | ----- | ----------- | 58 | | **select** | Params: `(emoji) => {}` | 59 | | **skin-change** | Params: `(skin) => {}` | 60 | 61 | 62 | #### I18n 63 | ```js 64 | search: 'Search', 65 | notfound: 'No Emoji Found', 66 | categories: { 67 | search: 'Search Results', 68 | recent: 'Frequently Used', 69 | people: 'Smileys & People', 70 | nature: 'Animals & Nature', 71 | foods: 'Food & Drink', 72 | activity: 'Activity', 73 | places: 'Travel & Places', 74 | objects: 'Objects', 75 | symbols: 'Symbols', 76 | flags: 'Flags', 77 | custom: 'Custom', 78 | } 79 | ``` 80 | 81 | #### Sheet sizes 82 | Sheets are served from [unpkg](https://unpkg.com), a global CDN that serves files published to [npm](https://www.npmjs.com). 83 | 84 | | Set | Size (`sheetSize: 16`) | Size (`sheetSize: 20`) | Size (`sheetSize: 32`) | Size (`sheetSize: 64`) | 85 | | --------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | 86 | | apple | 334 KB | 459 KB | 1.08 MB | 2.94 MB | 87 | | emojione | 315 KB | 435 KB | 1020 KB | 2.33 MB | 88 | | facebook | 322 KB | 439 KB | 1020 KB | 2.50 MB | 89 | | google | 301 KB | 409 KB | 907 KB | 2.17 MB | 90 | | messenger | 325 KB | 449 MB | 1.05 MB | 2.69 MB | 91 | | twitter | 288 KB | 389 KB | 839 KB | 1.82 MB | 92 | 93 | #### Datasets 94 | While all sets are available by default, you may want to include only a single set data to reduce the size of your bundle. 95 | 96 | | Set | Size (on disk) | 97 | | --------- | -------------- | 98 | | all | 570 KB | 99 | | apple | 484 KB | 100 | | emojione | 485 KB | 101 | | facebook | 421 KB | 102 | | google | 483 KB | 103 | | messenger | 197 KB | 104 | | twitter | 484 KB | 105 | 106 | To use these data files (or any other custom data), use the `NimblePicker` component: 107 | 108 | ```js 109 | import data from 'emoji-mart-vue/data/messenger.json' 110 | import { NimblePicker } from 'emoji-mart-vue' 111 | ``` 112 | 113 | ```html 114 | 115 | ``` 116 | 117 | #### Examples of `emoji` object: 118 | ```js 119 | { 120 | id: 'smiley', 121 | name: 'Smiling Face with Open Mouth', 122 | colons: ':smiley:', 123 | text: ':)', 124 | emoticons: [ 125 | '=)', 126 | '=-)' 127 | ], 128 | skin: null, 129 | native: '😃' 130 | } 131 | 132 | { 133 | id: 'santa', 134 | name: 'Father Christmas', 135 | colons: ':santa::skin-tone-3:', 136 | text: '', 137 | emoticons: [], 138 | skin: 3, 139 | native: '🎅🏼' 140 | } 141 | 142 | { 143 | id: 'octocat', 144 | name: 'Octocat', 145 | colons: ':octocat', 146 | text: '', 147 | emoticons: [], 148 | custom: true, 149 | imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' 150 | } 151 | 152 | ``` 153 | 154 | ### Emoji 155 | ```js 156 | import { Emoji } from 'emoji-mart-vue' 157 | ``` 158 | 159 | ```html 160 | 161 | 162 | 163 | ``` 164 | 165 | | Prop | Required | Default | Description | 166 | | ---- | :------: | ------- | ----------- | 167 | | **emoji** | ✓ | | Either a string or an `emoji` object | 168 | | **size** | ✓ | | The emoji width and height. | 169 | | **native** | | `false` | Renders the native unicode emoji | 170 | | [**fallback**](#unsupported-emojis-fallback) | | | Params: `(emoji) => {}` | 171 | | **set** | | `apple` | The emoji set: `'apple', 'google', 'twitter', 'emojione'` | 172 | | **sheetSize** | | `64` | The emoji [sheet size](#sheet-sizes): `16, 20, 32, 64` | 173 | | **backgroundImageFn** | | ```((set, sheetSize) => `https://unpkg.com/emoji-datasource@3.0.0/sheet_${set}_${sheetSize}.png`)``` | A Fn that returns that image sheet to use for emojis. Useful for avoiding a request if you have the sheet locally. | 174 | | **skin** | | `1` | Skin color: `1, 2, 3, 4, 5, 6` | 175 | | **tooltip** | | `false` | Show emoji short name when hovering (title) | 176 | 177 | | Event | Description | 178 | | ----- | ----------- | 179 | | **select** | Params: `(emoji) => {}` | 180 | | **mouseenter** | Params: `(emoji) => {}` | 181 | | **mouseleave** | Params: `(emoji) => {}` | 182 | 183 | #### Unsupported emojis fallback 184 | Certain sets don’t support all emojis (i.e. Messenger & Facebook don’t support `:shrug:`). By default the Emoji component will not render anything so that the emojis’ don’t take space in the picker when not available. When using the standalone Emoji component, you can however render anything you want by providing the `fallback` props. 185 | 186 | To have the component render `:shrug:` you would need to: 187 | 188 | ```js 189 | function emojiFallback(emoji) { 190 | return `:${emoji.short_names[0]}:` 191 | } 192 | ``` 193 | 194 | ```html 195 | 201 | ``` 202 | 203 | ## Custom emojis 204 | You can provide custom emojis which will show up in their own category. 205 | 206 | ```js 207 | import { Picker } from 'emoji-mart-vue' 208 | 209 | const customEmojis = [ 210 | { 211 | name: 'Octocat', 212 | short_names: ['octocat'], 213 | text: '', 214 | emoticons: [], 215 | keywords: ['github'], 216 | imageUrl: 'https://assets-cdn.github.com/images/icons/emoji/octocat.png?v7' 217 | } 218 | ] 219 | ``` 220 | 221 | ```html 222 | 223 | ``` 224 | 225 | ## Headless search 226 | The `Picker` doesn’t have to be mounted for you to take advantage of the advanced search results. 227 | 228 | ```js 229 | import { emojiIndex } from 'emoji-mart-vue' 230 | 231 | emojiIndex.search('christmas').map((o) => o.native) 232 | // => [🎄, 🎅🏼, 🔔, 🎁, ⛄️, ❄️] 233 | ``` 234 | 235 | ### With custom data 236 | ```js 237 | import data from 'emoji-mart-vue/data/messenger' 238 | import { NimbleEmojiIndex } from 'emoji-mart-vue' 239 | 240 | let emojiIndex = new NimbleEmojiIndex(data) 241 | emojiIndex.search('christmas') 242 | ``` 243 | 244 | ## Storage 245 | By default EmojiMart will store user chosen skin and frequently used emojis in `localStorage`. That can however be overwritten should you want to store these in your own storage. 246 | 247 | ```js 248 | import { store } from 'emoji-mart-vue' 249 | 250 | store.setHandlers({ 251 | getter: (key) => { 252 | // Get from your own storage (sync) 253 | }, 254 | 255 | setter: (key, value) => { 256 | // Persist in your own storage (can be async) 257 | } 258 | }) 259 | ``` 260 | 261 | Possible keys are: 262 | 263 | | Key | Value | Description | 264 | | --- | ----- | ----------- | 265 | | skin | `1, 2, 3, 4, 5, 6` | | 266 | | frequently | `{ 'astonished': 11, '+1': 22 }` | An object where the key is the emoji name and the value is the usage count | 267 | | last | 'astonished' | (Optional) Used by `frequently` to be sure the latest clicked emoji will always appear in the “Recent” category | 268 | 269 | ## Features 270 | ### Powerful search 271 | #### Short name, name and keywords 272 | Not only does **Emoji Mart** return more results than most emoji picker, they’re more accurate and sorted by relevance. 273 | 274 | summer 275 | 276 | #### Emoticons 277 | The only emoji picker that returns emojis when searching for emoticons. 278 | 279 | emoticons 280 | 281 | #### Results intersection 282 | For better results, **Emoji Mart** split search into words and only returns results matching both terms. 283 | 284 | high-five 285 | 286 | ### Fully customizable 287 | #### Anchors color, title and default emoji 288 | customizable-color
pick-your-emoji 289 | 290 | #### Emojis sizes and length 291 | size-and-length 292 | 293 | #### Default skin color 294 | As the developer, you have control over which skin color is used by default. 295 | 296 | skins 297 | 298 | It can however be overwritten as per user preference. 299 | 300 | customizable-skin 301 | 302 | #### Multiple sets supported 303 | Apple / Google / Twitter / EmojiOne / Messenger / Facebook 304 | 305 | sets 306 | 307 | ## Not opinionated 308 | **Emoji Mart** doesn’t automatically insert anything into a text input, nor does it show or hide itself. It simply returns an `emoji` object. It’s up to the developer to mount/unmount (it’s fast!) and position the picker. You can use the returned object as props for the `EmojiMart.Emoji` component. You could also use `emoji.colons` to insert text into a textarea or `emoji.native` to use the emoji. 309 | 310 | ## Development 311 | ```sh 312 | $ yarn build 313 | $ yarn start 314 | $ yarn storybook 315 | ``` 316 | 317 | ## 🎩 Hat tips! 318 | Powered by [iamcal/emoji-data](https://github.com/iamcal/emoji-data) and inspired by [iamcal/js-emoji](https://github.com/iamcal/js-emoji).
319 | 🙌🏼  [Cal Henderson](https://github.com/iamcal). 320 | -------------------------------------------------------------------------------- /css/emoji-mart.css: -------------------------------------------------------------------------------- 1 | .emoji-mart, 2 | .emoji-mart * { 3 | box-sizing: border-box; 4 | line-height: 1.15; 5 | } 6 | 7 | .emoji-mart { 8 | font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; 9 | font-size: 16px; 10 | display: inline-block; 11 | color: #222427; 12 | border: 1px solid #d9d9d9; 13 | border-radius: 5px; 14 | background: #fff; 15 | } 16 | 17 | .emoji-mart .emoji-mart-emoji { 18 | padding: 6px; 19 | } 20 | 21 | .emoji-mart-bar { 22 | border: 0 solid #d9d9d9; 23 | } 24 | .emoji-mart-bar:first-child { 25 | border-bottom-width: 1px; 26 | border-top-left-radius: 5px; 27 | border-top-right-radius: 5px; 28 | } 29 | .emoji-mart-bar:last-child { 30 | border-top-width: 1px; 31 | border-bottom-left-radius: 5px; 32 | border-bottom-right-radius: 5px; 33 | } 34 | 35 | .emoji-mart-anchors { 36 | display: flex; 37 | flex-direction: row; 38 | justify-content: space-between; 39 | padding: 0 6px; 40 | color: #858585; 41 | line-height: 0; 42 | } 43 | 44 | .emoji-mart-anchor { 45 | position: relative; 46 | display: block; 47 | flex: 1 1 auto; 48 | text-align: center; 49 | padding: 12px 4px; 50 | overflow: hidden; 51 | transition: color .1s ease-out; 52 | } 53 | .emoji-mart-anchor:hover, 54 | .emoji-mart-anchor-selected { 55 | color: #464646; 56 | } 57 | 58 | .emoji-mart-anchor-selected .emoji-mart-anchor-bar { 59 | bottom: 0; 60 | } 61 | 62 | .emoji-mart-anchor-bar { 63 | position: absolute; 64 | bottom: -3px; left: 0; 65 | width: 100%; height: 3px; 66 | background-color: #464646; 67 | } 68 | 69 | .emoji-mart-anchors i { 70 | display: inline-block; 71 | width: 100%; 72 | max-width: 22px; 73 | } 74 | 75 | .emoji-mart-anchors svg { 76 | fill: currentColor; 77 | max-height: 18px; 78 | } 79 | 80 | .emoji-mart-scroll { 81 | overflow-y: scroll; 82 | height: 270px; 83 | padding: 0 6px 6px 6px; 84 | will-change: transform; /* avoids "repaints on scroll" in mobile Chrome */ 85 | } 86 | 87 | .emoji-mart-search { 88 | margin-top: 6px; 89 | padding: 0 6px; 90 | } 91 | .emoji-mart-search input { 92 | font-size: 16px; 93 | display: block; 94 | width: 100%; 95 | padding: .2em .6em; 96 | border-radius: 25px; 97 | border: 1px solid #d9d9d9; 98 | outline: 0; 99 | } 100 | 101 | .emoji-mart-category .emoji-mart-emoji span { 102 | z-index: 1; 103 | position: relative; 104 | text-align: center; 105 | cursor: default; 106 | } 107 | 108 | .emoji-mart-category .emoji-mart-emoji:hover:before { 109 | z-index: 0; 110 | content: ""; 111 | position: absolute; 112 | top: 0; left: 0; 113 | width: 100%; height: 100%; 114 | background-color: #f4f4f4; 115 | border-radius: 100%; 116 | } 117 | 118 | .emoji-mart-category-label { 119 | z-index: 2; 120 | position: relative; 121 | position: -webkit-sticky; 122 | position: sticky; 123 | top: 0; 124 | } 125 | 126 | .emoji-mart-category-label span { 127 | display: block; 128 | width: 100%; 129 | font-weight: 500; 130 | padding: 5px 6px; 131 | background-color: #fff; 132 | background-color: rgba(255, 255, 255, .95); 133 | } 134 | 135 | .emoji-mart-emoji { 136 | position: relative; 137 | display: inline-block; 138 | font-size: 0; 139 | } 140 | 141 | .emoji-mart-no-results { 142 | font-size: 14px; 143 | text-align: center; 144 | padding-top: 70px; 145 | color: #858585; 146 | } 147 | .emoji-mart-no-results .emoji-mart-category-label { 148 | display: none; 149 | } 150 | .emoji-mart-no-results .emoji-mart-no-results-label { 151 | margin-top: .2em; 152 | } 153 | .emoji-mart-no-results .emoji-mart-emoji:hover:before { 154 | content: none; 155 | } 156 | 157 | .emoji-mart-preview { 158 | position: relative; 159 | height: 70px; 160 | } 161 | 162 | .emoji-mart-preview-emoji, 163 | .emoji-mart-preview-data, 164 | .emoji-mart-preview-skins { 165 | position: absolute; 166 | top: 50%; 167 | transform: translateY(-50%); 168 | } 169 | 170 | .emoji-mart-preview-emoji { 171 | left: 12px; 172 | } 173 | 174 | .emoji-mart-preview-data { 175 | left: 68px; right: 12px; 176 | word-break: break-all; 177 | } 178 | 179 | .emoji-mart-preview-skins { 180 | right: 30px; 181 | text-align: right; 182 | } 183 | 184 | .emoji-mart-preview-name { 185 | font-size: 14px; 186 | } 187 | 188 | .emoji-mart-preview-shortname { 189 | font-size: 12px; 190 | color: #888; 191 | } 192 | .emoji-mart-preview-shortname + .emoji-mart-preview-shortname, 193 | .emoji-mart-preview-shortname + .emoji-mart-preview-emoticon, 194 | .emoji-mart-preview-emoticon + .emoji-mart-preview-emoticon { 195 | margin-left: .5em; 196 | } 197 | 198 | .emoji-mart-preview-emoticon { 199 | font-size: 11px; 200 | color: #bbb; 201 | } 202 | 203 | .emoji-mart-title span { 204 | display: inline-block; 205 | vertical-align: middle; 206 | } 207 | 208 | .emoji-mart-title .emoji-mart-emoji { 209 | padding: 0; 210 | } 211 | 212 | .emoji-mart-title-label { 213 | color: #999A9C; 214 | font-size: 26px; 215 | font-weight: 300; 216 | } 217 | 218 | .emoji-mart-skin-swatches { 219 | font-size: 0; 220 | padding: 2px 0; 221 | border: 1px solid #d9d9d9; 222 | border-radius: 12px; 223 | background-color: #fff; 224 | } 225 | 226 | .emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch { 227 | width: 16px; 228 | padding: 0 2px; 229 | } 230 | 231 | .emoji-mart-skin-swatches-opened .emoji-mart-skin-swatch-selected:after { 232 | opacity: .75; 233 | } 234 | 235 | .emoji-mart-skin-swatch { 236 | display: inline-block; 237 | width: 0; 238 | vertical-align: middle; 239 | transition-property: width, padding; 240 | transition-duration: .125s; 241 | transition-timing-function: ease-out; 242 | } 243 | 244 | .emoji-mart-skin-swatch:nth-child(1) { transition-delay: 0s } 245 | .emoji-mart-skin-swatch:nth-child(2) { transition-delay: .03s } 246 | .emoji-mart-skin-swatch:nth-child(3) { transition-delay: .06s } 247 | .emoji-mart-skin-swatch:nth-child(4) { transition-delay: .09s } 248 | .emoji-mart-skin-swatch:nth-child(5) { transition-delay: .12s } 249 | .emoji-mart-skin-swatch:nth-child(6) { transition-delay: .15s } 250 | 251 | .emoji-mart-skin-swatch-selected { 252 | position: relative; 253 | width: 16px; 254 | padding: 0 2px; 255 | } 256 | .emoji-mart-skin-swatch-selected:after { 257 | content: ""; 258 | position: absolute; 259 | top: 50%; left: 50%; 260 | width: 4px; height: 4px; 261 | margin: -2px 0 0 -2px; 262 | background-color: #fff; 263 | border-radius: 100%; 264 | pointer-events: none; 265 | opacity: 0; 266 | transition: opacity .2s ease-out; 267 | } 268 | 269 | .emoji-mart-skin { 270 | display: inline-block; 271 | width: 100%; padding-top: 100%; 272 | max-width: 12px; 273 | border-radius: 100%; 274 | } 275 | 276 | .emoji-mart-skin-tone-1 { background-color: #ffc93a } 277 | .emoji-mart-skin-tone-2 { background-color: #fadcbc } 278 | .emoji-mart-skin-tone-3 { background-color: #e0bb95 } 279 | .emoji-mart-skin-tone-4 { background-color: #bf8f68 } 280 | .emoji-mart-skin-tone-5 { background-color: #9b643d } 281 | .emoji-mart-skin-tone-6 { background-color: #594539 } 282 | -------------------------------------------------------------------------------- /docs/app.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 83 | 84 | -------------------------------------------------------------------------------- /docs/images/parrot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jm-david/emoji-mart-vue/e441565fe4cd842322792bf04b09a71c55a102d0/docs/images/parrot.gif -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Emoji Mart Vue 🏬 | One component to pick them all 5 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './app' 3 | 4 | new Vue({ 5 | el: '#app', 6 | render: h => h(App) 7 | }) 8 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var pack = require('../package.json') 3 | var webpack = require('webpack') 4 | 5 | var PROD = process.env.NODE_ENV === 'production' 6 | var TEST = process.env.NODE_ENV === 'test' 7 | 8 | var config = { 9 | entry: path.resolve('docs/index.js'), 10 | output: { 11 | path: path.resolve('docs'), 12 | filename: 'bundle.js', 13 | library: 'EmojiMart', 14 | libraryTarget: 'umd', 15 | }, 16 | 17 | externals: [], 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | include: [ 25 | path.resolve('src'), 26 | path.resolve('docs'), 27 | ], 28 | }, 29 | { 30 | test: /\.vue$/, 31 | loader: 'vue-loader', 32 | include: [ 33 | path.resolve('src'), 34 | path.resolve('docs'), 35 | ] 36 | }, 37 | { 38 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 39 | loader: 'url-loader', 40 | options: { 41 | limit: 10000 42 | } 43 | } 44 | ], 45 | }, 46 | 47 | resolve: { 48 | extensions: ['.vue', '.js'], 49 | }, 50 | 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | EMOJI_DATASOURCE_VERSION: `'${pack.devDependencies['emoji-datasource']}'`, 54 | }), 55 | ], 56 | 57 | bail: true, 58 | } 59 | 60 | module.exports = config 61 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Jan 27 2017 13:33:03 GMT-0700 (MST) 3 | var webpackConfig = require('./spec/webpack.config.js'); 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: '', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'spec/*spec.js', 20 | ], 21 | 22 | 23 | // list of files to exclude 24 | exclude: [ 25 | ], 26 | 27 | 28 | // preprocess matching files before serving them to the browser 29 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 30 | preprocessors: { 31 | 'spec/*spec.js': ['webpack'], 32 | }, 33 | 34 | 35 | // test results reporter to use 36 | // possible values: 'dots', 'progress' 37 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 38 | reporters: ['progress'], 39 | 40 | 41 | // web server port 42 | port: 9876, 43 | 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 51 | logLevel: config.LOG_INFO, 52 | 53 | 54 | // enable / disable watching file and executing tests whenever any file changes 55 | autoWatch: true, 56 | 57 | 58 | // start these browsers 59 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 60 | browsers: ['Chrome'], 61 | 62 | 63 | // Continuous Integration mode 64 | // if true, Karma captures browsers, runs the tests and exits 65 | singleRun: true, 66 | 67 | // Concurrency level 68 | // how many browser should be started simultaneous 69 | concurrency: Infinity, 70 | 71 | webpack: webpackConfig, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-mart-vue", 3 | "version": "2.6.6", 4 | "description": "Customizable Slack-like emoji picker for VueJS", 5 | "main": "dist/emoji-mart.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:jm-david/emoji-mart-vue.git" 9 | }, 10 | "keywords": [ 11 | "vue", 12 | "vuejs", 13 | "emoji", 14 | "picker" 15 | ], 16 | "author": "Etienne Lemay", 17 | "license": "BSD-3-Clause", 18 | "bugs": { 19 | "url": "https://github.com/jm-david/emoji-mart-vue/issues" 20 | }, 21 | "homepage": "https://github.com/jm-david/emoji-mart-vue", 22 | "dependencies": { 23 | "postcss-loader": "^3.0.0" 24 | }, 25 | "peerDependencies": { 26 | "vue": "^2.0.0" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.26.0", 30 | "babel-core": "6.7.2", 31 | "babel-loader": "^7.1.2", 32 | "babel-plugin-module-resolver": "2.7.1", 33 | "babel-plugin-transform-define": "^1.3.0", 34 | "babel-plugin-transform-es2015-destructuring": "6.9.0", 35 | "babel-plugin-transform-object-rest-spread": "6.8.0", 36 | "babel-plugin-transform-runtime": "^6.23.0", 37 | "babel-preset-es2015": "6.6.0", 38 | "babel-runtime": "^6.26.0", 39 | "css-loader": "^0.28.0", 40 | "emoji-datasource": "4.0.4", 41 | "emojilib": "^2.2.1", 42 | "inflection": "1.10.0", 43 | "jasmine-core": "^2.5.2", 44 | "karma": "^1.4.0", 45 | "karma-chrome-launcher": "^2.0.0", 46 | "karma-cli": "^1.0.1", 47 | "karma-jasmine": "^1.1.0", 48 | "karma-webpack": "^2.0.4", 49 | "mkdirp": "0.5.1", 50 | "prettier": "1.11.1", 51 | "rimraf": "2.5.2", 52 | "size-limit": "^0.11.4", 53 | "url-loader": "^0.5.8", 54 | "vue": "^2.5.2", 55 | "vue-loader": "^13.3.0", 56 | "vue-style-loader": "^4.1.2", 57 | "vue-template-compiler": "^2.5.2", 58 | "webpack": "^3.6.0", 59 | "webpack-dev-server": "^2.9.1" 60 | }, 61 | "scripts": { 62 | "clean": "rm -rf dist/", 63 | "build:data": "node scripts/build-data", 64 | "build:dist": "webpack --config src/webpack.config.js", 65 | "build:docs": "webpack --config docs/webpack.config.js", 66 | "build": "npm run clean && npm run build:data && npm run build:dist", 67 | "dev:docs": "webpack -w --config docs/webpack.config.js", 68 | "start": "npm run dev:docs", 69 | "stats": "webpack --config ./spec/webpack.config.js --json > spec/stats.json", 70 | "test": "NODE_ENV=test karma start && size-limit", 71 | "prepublishOnly": "npm run build", 72 | "prettier": "prettier --write \"{src,spec}/**/*.js\"" 73 | }, 74 | "size-limit": [ 75 | { 76 | "path": "dist/emoji-mart.js", 77 | "limit": "80 KB" 78 | } 79 | ], 80 | "postcss": { 81 | "plugins": { 82 | "autoprefixer": {} 83 | } 84 | }, 85 | "browserslist": [ 86 | "last 3 version", 87 | "IE >= 11", 88 | "iOS >= 9" 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /scripts/build-data.js: -------------------------------------------------------------------------------- 1 | const build = require('./build') 2 | const sets = ['apple', 'emojione', 'facebook', 'google', 'messenger', 'twitter'] 3 | 4 | build({ output: 'data/all.json' }) 5 | 6 | sets.forEach((set) => { 7 | build({ 8 | output: `data/${set}.json`, 9 | sets: [set], 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | emojiLib = require('emojilib'), 3 | inflection = require('inflection'), 4 | mkdirp = require('mkdirp') 5 | 6 | var { compress } = require('../src/utils/data') 7 | 8 | var categories = [ 9 | ['Smileys & People', 'people'], 10 | ['Animals & Nature', 'nature'], 11 | ['Food & Drink', 'foods'], 12 | ['Activities', 'activity'], 13 | ['Travel & Places', 'places'], 14 | ['Objects', 'objects'], 15 | ['Symbols', 'symbols'], 16 | ['Flags', 'flags'], 17 | ] 18 | 19 | var sets = ['apple', 'emojione', 'facebook', 'google', 'messenger', 'twitter'] 20 | 21 | module.exports = (options) => { 22 | delete require.cache[require.resolve('emoji-datasource')] 23 | var emojiData = require('emoji-datasource') 24 | 25 | var data = { compressed: true, categories: [], emojis: {}, aliases: {} }, 26 | categoriesIndex = {} 27 | 28 | categories.forEach((category, i) => { 29 | let [name, id] = category 30 | data.categories[i] = { id: id, name: name, emojis: [] } 31 | categoriesIndex[name] = i 32 | }) 33 | 34 | emojiData.sort((a, b) => { 35 | var aTest = a.sort_order || a.short_name, 36 | bTest = b.sort_order || b.short_name 37 | 38 | return aTest - bTest 39 | }) 40 | 41 | emojiData.forEach((datum) => { 42 | var category = datum.category, 43 | keywords = [], 44 | categoryIndex 45 | 46 | if (!datum.category) { 47 | throw new Error('“' + datum.short_name + '” doesn’t have a category') 48 | } 49 | 50 | if (options.sets) { 51 | var keepEmoji = false 52 | 53 | options.sets.forEach((set) => { 54 | if (keepEmoji) return 55 | if (datum[`has_img_${set}`]) { 56 | keepEmoji = true 57 | } 58 | }) 59 | 60 | if (!keepEmoji) { 61 | return 62 | } 63 | 64 | sets.forEach((set) => { 65 | if (options.sets.length == 1 || options.sets.indexOf(set) == -1) { 66 | var key = `has_img_${set}` 67 | delete datum[key] 68 | } 69 | }) 70 | } 71 | 72 | datum.name || (datum.name = datum.short_name.replace(/\-/g, ' ')) 73 | datum.name = inflection.titleize(datum.name || '') 74 | 75 | if (!datum.name) { 76 | throw new Error('“' + datum.short_name + '” doesn’t have a name') 77 | } 78 | 79 | datum.emoticons = datum.texts || [] 80 | datum.text = datum.text || '' 81 | delete datum.texts 82 | 83 | if (emojiLib.lib[datum.short_name]) { 84 | datum.keywords = emojiLib.lib[datum.short_name].keywords 85 | } 86 | 87 | if (datum.category != 'Skin Tones') { 88 | categoryIndex = categoriesIndex[category] 89 | data.categories[categoryIndex].emojis.push(datum.short_name) 90 | data.emojis[datum.short_name] = datum 91 | } 92 | 93 | datum.short_names.forEach((short_name, i) => { 94 | if (i == 0) { 95 | return 96 | } 97 | 98 | data.aliases[short_name] = datum.short_name 99 | }) 100 | 101 | delete datum.docomo 102 | delete datum.au 103 | delete datum.softbank 104 | delete datum.google 105 | delete datum.image 106 | delete datum.category 107 | delete datum.sort_order 108 | 109 | compress(datum) 110 | }) 111 | 112 | var flags = data.categories[categoriesIndex['Flags']] 113 | flags.emojis = flags.emojis 114 | .filter((flag) => { 115 | // Until browsers support Flag UN 116 | if (flag == 'flag-un') return 117 | return true 118 | }) 119 | .sort() 120 | 121 | fs.writeFile(options.output, JSON.stringify(data), (err) => { 122 | if (err) throw err 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /scripts/define.js: -------------------------------------------------------------------------------- 1 | var pack = require('../package.json') 2 | 3 | module.exports = { 4 | 'process.env.NODE_ENV': 'production', 5 | EMOJI_DATASOURCE_VERSION: pack.devDependencies['emoji-datasource'], 6 | } 7 | -------------------------------------------------------------------------------- /spec/emoji-index-spec.js: -------------------------------------------------------------------------------- 1 | import emojiIndex from '../src/utils/emoji-index/emoji-index' 2 | 3 | describe('#emojiIndex', () => { 4 | describe('search', function() { 5 | it('should work', () => { 6 | expect(emojiIndex.search('pineapple')).toEqual([ 7 | { 8 | id: 'pineapple', 9 | name: 'Pineapple', 10 | colons: ':pineapple:', 11 | emoticons: [], 12 | unified: '1f34d', 13 | skin: null, 14 | native: '🍍', 15 | }, 16 | ]) 17 | }) 18 | 19 | it('should filter only emojis we care about, exclude pineapple', () => { 20 | let emojisToShowFilter = (data) => { 21 | data.unified !== '1F34D' 22 | } 23 | expect( 24 | emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id), 25 | ).not.toContain('pineapple') 26 | }) 27 | 28 | it('can include/exclude categories', () => { 29 | expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]) 30 | }) 31 | 32 | it('can search for thinking_face', () => { 33 | expect(emojiIndex.search('thinking_fac').map((x) => x.id)).toEqual([ 34 | 'thinking_face', 35 | ]) 36 | }) 37 | 38 | it('can search for woman-facepalming', () => { 39 | expect(emojiIndex.search('woman-facep').map((x) => x.id)).toEqual([ 40 | 'woman-facepalming', 41 | ]) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /spec/picker-spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TestUtils from 'react-dom/test-utils' 3 | 4 | import data from '../data/all.json' 5 | import { NimblePicker } from '../src/components' 6 | 7 | const { click } = TestUtils.Simulate 8 | 9 | const { 10 | renderIntoDocument, 11 | scryRenderedComponentsWithType, 12 | findRenderedComponentWithType, 13 | } = TestUtils 14 | 15 | const render = (props = {}) => { 16 | const defaultProps = { data } 17 | return renderIntoDocument() 18 | } 19 | 20 | describe('NimblePicker', () => { 21 | let subject 22 | 23 | it('works', () => { 24 | subject = render() 25 | expect(subject).toBeDefined() 26 | }) 27 | 28 | describe('categories', () => { 29 | it('shows 10 by default', () => { 30 | subject = render() 31 | expect(subject.categories.length).toEqual(10) 32 | }) 33 | 34 | it('will not show some based upon our filter', () => { 35 | subject = render({ emojisToShowFilter: (unified) => false }) 36 | expect(subject.categories.length).toEqual(2) 37 | }) 38 | 39 | it('maintains category ids after it is filtered', () => { 40 | subject = render({ emojisToShowFilter: (emoji) => true }) 41 | const categoriesWithIds = subject.categories.filter( 42 | (category) => category.id, 43 | ) 44 | expect(categoriesWithIds.length).toEqual(10) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /spec/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var pack = require('../package.json') 3 | var webpack = require('webpack') 4 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 5 | .BundleAnalyzerPlugin 6 | 7 | var PROD = process.env.NODE_ENV === 'production' 8 | var TEST = process.env.NODE_ENV === 'test' 9 | 10 | var config = { 11 | entry: path.resolve('src/index.js'), 12 | output: { 13 | path: path.resolve('spec'), 14 | filename: 'bundle.js', 15 | library: 'EmojiMart', 16 | libraryTarget: 'umd', 17 | }, 18 | 19 | externals: [], 20 | 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | use: 'babel-loader', 26 | include: [path.resolve('src'), path.resolve('spec')], 27 | }, 28 | ], 29 | }, 30 | 31 | resolve: { 32 | extensions: ['.js'], 33 | }, 34 | 35 | plugins: [ 36 | new webpack.DefinePlugin({ 37 | EMOJI_DATASOURCE_VERSION: `'${pack.devDependencies['emoji-datasource']}'`, 38 | }), 39 | ], 40 | 41 | bail: true, 42 | } 43 | 44 | if (!TEST) { 45 | config.externals = config.externals.concat([ 46 | { 47 | react: { 48 | root: 'React', 49 | commonjs2: 'react', 50 | commonjs: 'react', 51 | amd: 'react', 52 | }, 53 | }, 54 | ]) 55 | 56 | config.plugins = config.plugins.concat([ 57 | new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false }), 58 | ]) 59 | } 60 | 61 | module.exports = config 62 | -------------------------------------------------------------------------------- /src/components/anchors.old.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import SVGs from '../svgs' 5 | 6 | export default class Anchors extends React.PureComponent { 7 | constructor(props) { 8 | super(props) 9 | 10 | let defaultCategory = props.categories.filter( 11 | (category) => category.first, 12 | )[0] 13 | 14 | this.state = { 15 | selected: defaultCategory.name, 16 | } 17 | 18 | this.handleClick = this.handleClick.bind(this) 19 | } 20 | 21 | getSVG(id) { 22 | this.SVGs || (this.SVGs = {}) 23 | 24 | if (this.SVGs[id]) { 25 | return this.SVGs[id] 26 | } else { 27 | let svg = ` 28 | ${SVGs[id]} 29 | ` 30 | 31 | this.SVGs[id] = svg 32 | return svg 33 | } 34 | } 35 | 36 | handleClick(e) { 37 | var index = e.currentTarget.getAttribute('data-index') 38 | var { categories, onAnchorClick } = this.props 39 | 40 | onAnchorClick(categories[index], index) 41 | } 42 | 43 | render() { 44 | var { categories, onAnchorClick, color, i18n } = this.props, 45 | { selected } = this.state 46 | 47 | return ( 48 |
49 | {categories.map((category, i) => { 50 | var { id, name, anchor } = category, 51 | isSelected = name == selected 52 | 53 | if (anchor === false) { 54 | return null 55 | } 56 | 57 | return ( 58 | 68 |
69 | 73 | 74 | ) 75 | })} 76 |
77 | ) 78 | } 79 | } 80 | 81 | Anchors.propTypes = { 82 | categories: PropTypes.array, 83 | onAnchorClick: PropTypes.func, 84 | } 85 | 86 | Anchors.defaultProps = { 87 | categories: [], 88 | onAnchorClick: () => {}, 89 | } 90 | -------------------------------------------------------------------------------- /src/components/anchors.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 48 | 49 | 92 | 93 | 101 | -------------------------------------------------------------------------------- /src/components/category.old.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import frequently from '../utils/frequently' 5 | import { getData } from '../utils' 6 | import { NimbleEmoji } from '.' 7 | 8 | export default class Category extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | 12 | this.data = props.data 13 | this.setContainerRef = this.setContainerRef.bind(this) 14 | this.setLabelRef = this.setLabelRef.bind(this) 15 | } 16 | 17 | componentDidMount() { 18 | this.parent = this.container.parentNode 19 | 20 | this.margin = 0 21 | this.minMargin = 0 22 | 23 | this.memoizeSize() 24 | } 25 | 26 | shouldComponentUpdate(nextProps, nextState) { 27 | var { 28 | name, 29 | perLine, 30 | native, 31 | hasStickyPosition, 32 | emojis, 33 | emojiProps, 34 | } = this.props, 35 | { skin, size, set } = emojiProps, 36 | { 37 | perLine: nextPerLine, 38 | native: nextNative, 39 | hasStickyPosition: nextHasStickyPosition, 40 | emojis: nextEmojis, 41 | emojiProps: nextEmojiProps, 42 | } = nextProps, 43 | { skin: nextSkin, size: nextSize, set: nextSet } = nextEmojiProps, 44 | shouldUpdate = false 45 | 46 | if (name == 'Recent' && perLine != nextPerLine) { 47 | shouldUpdate = true 48 | } 49 | 50 | if (name == 'Search') { 51 | shouldUpdate = !(emojis == nextEmojis) 52 | } 53 | 54 | if ( 55 | skin != nextSkin || 56 | size != nextSize || 57 | native != nextNative || 58 | set != nextSet || 59 | hasStickyPosition != nextHasStickyPosition 60 | ) { 61 | shouldUpdate = true 62 | } 63 | 64 | return shouldUpdate 65 | } 66 | 67 | memoizeSize() { 68 | var { top, height } = this.container.getBoundingClientRect() 69 | var { top: parentTop } = this.parent.getBoundingClientRect() 70 | var { height: labelHeight } = this.label.getBoundingClientRect() 71 | 72 | this.top = top - parentTop + this.parent.scrollTop 73 | 74 | if (height == 0) { 75 | this.maxMargin = 0 76 | } else { 77 | this.maxMargin = height - labelHeight 78 | } 79 | } 80 | 81 | handleScroll(scrollTop) { 82 | var margin = scrollTop - this.top 83 | margin = margin < this.minMargin ? this.minMargin : margin 84 | margin = margin > this.maxMargin ? this.maxMargin : margin 85 | 86 | if (margin == this.margin) return 87 | 88 | if (!this.props.hasStickyPosition) { 89 | this.label.style.top = `${margin}px` 90 | } 91 | 92 | this.margin = margin 93 | return true 94 | } 95 | 96 | getEmojis() { 97 | var { name, emojis, recent, perLine } = this.props 98 | 99 | if (name == 'Recent') { 100 | let { custom } = this.props 101 | let frequentlyUsed = recent || frequently.get(perLine) 102 | 103 | if (frequentlyUsed.length) { 104 | emojis = frequentlyUsed 105 | .map((id) => { 106 | const emoji = custom.filter((e) => e.id === id)[0] 107 | if (emoji) { 108 | return emoji 109 | } 110 | 111 | return id 112 | }) 113 | .filter((id) => !!getData(id, null, null, this.data)) 114 | } 115 | 116 | if (emojis.length === 0 && frequentlyUsed.length > 0) { 117 | return null 118 | } 119 | } 120 | 121 | if (emojis) { 122 | emojis = emojis.slice(0) 123 | } 124 | 125 | return emojis 126 | } 127 | 128 | updateDisplay(display) { 129 | var emojis = this.getEmojis() 130 | 131 | if (!emojis) { 132 | return 133 | } 134 | 135 | this.container.style.display = display 136 | } 137 | 138 | setContainerRef(c) { 139 | this.container = c 140 | } 141 | 142 | setLabelRef(c) { 143 | this.label = c 144 | } 145 | 146 | render() { 147 | var { id, name, hasStickyPosition, emojiProps, i18n } = this.props, 148 | emojis = this.getEmojis(), 149 | labelStyles = {}, 150 | labelSpanStyles = {}, 151 | containerStyles = {} 152 | 153 | if (!emojis) { 154 | containerStyles = { 155 | display: 'none', 156 | } 157 | } 158 | 159 | if (!hasStickyPosition) { 160 | labelStyles = { 161 | height: 28, 162 | } 163 | 164 | labelSpanStyles = { 165 | position: 'absolute', 166 | } 167 | } 168 | 169 | return ( 170 |
177 |
182 | 183 | {i18n.categories[id]} 184 | 185 |
186 | 187 | {emojis && 188 | emojis.map((emoji) => 189 | NimbleEmoji({ emoji: emoji, data: this.data, ...emojiProps }), 190 | )} 191 | 192 | {emojis && 193 | !emojis.length && ( 194 |
195 |
196 | {NimbleEmoji({ 197 | data: this.data, 198 | ...emojiProps, 199 | size: 38, 200 | emoji: 'sleuth_or_spy', 201 | onOver: null, 202 | onLeave: null, 203 | onClick: null, 204 | })} 205 |
206 | 207 |
{i18n.notfound}
208 |
209 | )} 210 |
211 | ) 212 | } 213 | } 214 | 215 | Category.propTypes = { 216 | emojis: PropTypes.array, 217 | hasStickyPosition: PropTypes.bool, 218 | name: PropTypes.string.isRequired, 219 | native: PropTypes.bool.isRequired, 220 | perLine: PropTypes.number.isRequired, 221 | emojiProps: PropTypes.object.isRequired, 222 | recent: PropTypes.arrayOf(PropTypes.string), 223 | } 224 | 225 | Category.defaultProps = { 226 | emojis: [], 227 | hasStickyPosition: true, 228 | } 229 | -------------------------------------------------------------------------------- /src/components/category.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 90 | 91 | 154 | 155 | 165 | -------------------------------------------------------------------------------- /src/components/emoji/emoji.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import data from '../../../data/all.json' 4 | import NimbleEmoji from './nimble-emoji' 5 | 6 | import { EmojiPropTypes, EmojiDefaultProps } from '../../utils/shared-props' 7 | 8 | const Emoji = (props) => { 9 | for (let k in Emoji.defaultProps) { 10 | if (props[k] == undefined && Emoji.defaultProps[k] != undefined) { 11 | props[k] = Emoji.defaultProps[k] 12 | } 13 | } 14 | 15 | return NimbleEmoji({ ...props }) 16 | } 17 | 18 | Emoji.propTypes = EmojiPropTypes 19 | Emoji.defaultProps = { ...EmojiDefaultProps, data } 20 | 21 | export default Emoji 22 | -------------------------------------------------------------------------------- /src/components/emoji/emoji.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/components/emoji/nimble-emoji.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { getData, getSanitizedData, unifiedToNative } from '../../utils' 5 | import { uncompress } from '../../utils/data' 6 | import { EmojiPropTypes, EmojiDefaultProps } from '../../utils/shared-props' 7 | 8 | const SHEET_COLUMNS = 52 9 | 10 | const _getData = (props) => { 11 | var { emoji, skin, set, data } = props 12 | return getData(emoji, skin, set, data) 13 | } 14 | 15 | const _getPosition = (props) => { 16 | var { sheet_x, sheet_y } = _getData(props), 17 | multiply = 100 / (SHEET_COLUMNS - 1) 18 | 19 | return `${multiply * sheet_x}% ${multiply * sheet_y}%` 20 | } 21 | 22 | const _getSanitizedData = (props) => { 23 | var { emoji, skin, set, data } = props 24 | return getSanitizedData(emoji, skin, set, data) 25 | } 26 | 27 | const _handleClick = (e, props) => { 28 | if (!props.onClick) { 29 | return 30 | } 31 | var { onClick } = props, 32 | emoji = _getSanitizedData(props) 33 | 34 | onClick(emoji, e) 35 | } 36 | 37 | const _handleOver = (e, props) => { 38 | if (!props.onOver) { 39 | return 40 | } 41 | var { onOver } = props, 42 | emoji = _getSanitizedData(props) 43 | 44 | onOver(emoji, e) 45 | } 46 | 47 | const _handleLeave = (e, props) => { 48 | if (!props.onLeave) { 49 | return 50 | } 51 | var { onLeave } = props, 52 | emoji = _getSanitizedData(props) 53 | 54 | onLeave(emoji, e) 55 | } 56 | 57 | const _isNumeric = (value) => { 58 | return !isNaN(value - parseFloat(value)) 59 | } 60 | 61 | const _convertStyleToCSS = (style) => { 62 | let div = document.createElement('div') 63 | 64 | for (let key in style) { 65 | let value = style[key] 66 | 67 | if (_isNumeric(value)) { 68 | value += 'px' 69 | } 70 | 71 | div.style[key] = value 72 | } 73 | 74 | return div.getAttribute('style') 75 | } 76 | 77 | const NimbleEmoji = (props) => { 78 | if (props.data.compressed) { 79 | uncompress(props.data) 80 | } 81 | 82 | for (let k in NimbleEmoji.defaultProps) { 83 | if (props[k] == undefined && NimbleEmoji.defaultProps[k] != undefined) { 84 | props[k] = NimbleEmoji.defaultProps[k] 85 | } 86 | } 87 | 88 | let data = _getData(props) 89 | if (!data) { 90 | return null 91 | } 92 | 93 | let { unified, custom, short_names, imageUrl } = data, 94 | style = {}, 95 | children = props.children, 96 | className = 'emoji-mart-emoji', 97 | title = null 98 | 99 | if (!unified && !custom) { 100 | return null 101 | } 102 | 103 | if (props.tooltip) { 104 | title = short_names[0] 105 | } 106 | 107 | if (props.native && unified) { 108 | className += ' emoji-mart-emoji-native' 109 | style = { fontSize: props.size } 110 | children = unifiedToNative(unified) 111 | 112 | if (props.forceSize) { 113 | style.display = 'inline-block' 114 | style.width = props.size 115 | style.height = props.size 116 | } 117 | } else if (custom) { 118 | className += ' emoji-mart-emoji-custom' 119 | style = { 120 | width: props.size, 121 | height: props.size, 122 | display: 'inline-block', 123 | backgroundImage: `url(${imageUrl})`, 124 | backgroundSize: 'contain', 125 | } 126 | } else { 127 | let setHasEmoji = 128 | data[`has_img_${props.set}`] == undefined || data[`has_img_${props.set}`] 129 | 130 | if (!setHasEmoji) { 131 | if (props.fallback) { 132 | return props.fallback(data) 133 | } else { 134 | return null 135 | } 136 | } else { 137 | style = { 138 | width: props.size, 139 | height: props.size, 140 | display: 'inline-block', 141 | backgroundImage: `url(${props.backgroundImageFn( 142 | props.set, 143 | props.sheetSize, 144 | )})`, 145 | backgroundSize: `${100 * SHEET_COLUMNS}%`, 146 | backgroundPosition: _getPosition(props), 147 | } 148 | } 149 | } 150 | 151 | if (props.html) { 152 | style = _convertStyleToCSS(style) 153 | return `${children || ''}` 156 | } else { 157 | return ( 158 | _handleClick(e, props)} 161 | onMouseEnter={(e) => _handleOver(e, props)} 162 | onMouseLeave={(e) => _handleLeave(e, props)} 163 | title={title} 164 | className={className} 165 | > 166 | {children} 167 | 168 | ) 169 | } 170 | } 171 | 172 | NimbleEmoji.propTypes = { ...EmojiPropTypes, data: PropTypes.object.isRequired } 173 | NimbleEmoji.defaultProps = EmojiDefaultProps 174 | 175 | export default NimbleEmoji 176 | -------------------------------------------------------------------------------- /src/components/emoji/nimbleEmoji.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 123 | 124 | 133 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Anchors } from './anchors' 2 | export { default as Category } from './category' 3 | export { default as Preview } from './preview' 4 | export { default as Search } from './search' 5 | export { default as Skins } from './skins' 6 | 7 | export { default as Emoji } from './emoji/emoji' 8 | export { default as NimbleEmoji } from './emoji/nimbleEmoji' 9 | 10 | export { default as Picker } from './picker/picker' 11 | export { default as NimblePicker } from './picker/nimblePicker' 12 | -------------------------------------------------------------------------------- /src/components/picker/nimble-picker.js: -------------------------------------------------------------------------------- 1 | import '../../vendor/raf-polyfill' 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | import store from '../../utils/store' 7 | import frequently from '../../utils/frequently' 8 | import { deepMerge, measureScrollbar } from '../../utils' 9 | import { uncompress } from '../../utils/data' 10 | import { PickerPropTypes, PickerDefaultProps } from '../../utils/shared-props' 11 | 12 | import { Anchors, Category, Preview, Search } from '..' 13 | 14 | const I18N = { 15 | search: 'Search', 16 | notfound: 'No Emoji Found', 17 | categories: { 18 | search: 'Search Results', 19 | recent: 'Frequently Used', 20 | people: 'Smileys & People', 21 | nature: 'Animals & Nature', 22 | foods: 'Food & Drink', 23 | activity: 'Activity', 24 | places: 'Travel & Places', 25 | objects: 'Objects', 26 | symbols: 'Symbols', 27 | flags: 'Flags', 28 | custom: 'Custom', 29 | }, 30 | } 31 | 32 | export default class NimblePicker extends React.PureComponent { 33 | constructor(props) { 34 | super(props) 35 | 36 | this.RECENT_CATEGORY = { id: 'recent', name: 'Recent', emojis: null } 37 | this.CUSTOM_CATEGORY = { id: 'custom', name: 'Custom', emojis: [] } 38 | this.SEARCH_CATEGORY = { 39 | id: 'search', 40 | name: 'Search', 41 | emojis: null, 42 | anchor: false, 43 | } 44 | 45 | if (props.data.compressed) { 46 | uncompress(props.data) 47 | } 48 | 49 | this.data = props.data 50 | this.i18n = deepMerge(I18N, props.i18n) 51 | this.state = { 52 | skin: props.skin || store.get('skin') || props.defaultSkin, 53 | firstRender: true, 54 | } 55 | 56 | this.categories = [] 57 | let allCategories = [].concat(this.data.categories) 58 | 59 | if (props.custom.length > 0) { 60 | this.CUSTOM_CATEGORY.emojis = props.custom.map((emoji) => { 61 | return { 62 | ...emoji, 63 | // `` expects emoji to have an `id`. 64 | id: emoji.short_names[0], 65 | custom: true, 66 | } 67 | }) 68 | 69 | allCategories.push(this.CUSTOM_CATEGORY) 70 | } 71 | 72 | this.hideRecent = true 73 | 74 | if (props.include != undefined) { 75 | allCategories.sort((a, b) => { 76 | if (props.include.indexOf(a.id) > props.include.indexOf(b.id)) { 77 | return 1 78 | } 79 | 80 | return -1 81 | }) 82 | } 83 | 84 | for ( 85 | let categoryIndex = 0; 86 | categoryIndex < allCategories.length; 87 | categoryIndex++ 88 | ) { 89 | const category = allCategories[categoryIndex] 90 | let isIncluded = 91 | props.include && props.include.length 92 | ? props.include.indexOf(category.id) > -1 93 | : true 94 | let isExcluded = 95 | props.exclude && props.exclude.length 96 | ? props.exclude.indexOf(category.id) > -1 97 | : false 98 | if (!isIncluded || isExcluded) { 99 | continue 100 | } 101 | 102 | if (props.emojisToShowFilter) { 103 | let newEmojis = [] 104 | 105 | const { emojis } = category 106 | for (let emojiIndex = 0; emojiIndex < emojis.length; emojiIndex++) { 107 | const emoji = emojis[emojiIndex] 108 | if (props.emojisToShowFilter(this.data.emojis[emoji] || emoji)) { 109 | newEmojis.push(emoji) 110 | } 111 | } 112 | 113 | if (newEmojis.length) { 114 | let newCategory = { 115 | emojis: newEmojis, 116 | name: category.name, 117 | id: category.id, 118 | } 119 | 120 | this.categories.push(newCategory) 121 | } 122 | } else { 123 | this.categories.push(category) 124 | } 125 | } 126 | 127 | let includeRecent = 128 | props.include && props.include.length 129 | ? props.include.indexOf(this.RECENT_CATEGORY.id) > -1 130 | : true 131 | let excludeRecent = 132 | props.exclude && props.exclude.length 133 | ? props.exclude.indexOf(this.RECENT_CATEGORY.id) > -1 134 | : false 135 | if (includeRecent && !excludeRecent) { 136 | this.hideRecent = false 137 | this.categories.unshift(this.RECENT_CATEGORY) 138 | } 139 | 140 | if (this.categories[0]) { 141 | this.categories[0].first = true 142 | } 143 | 144 | this.categories.unshift(this.SEARCH_CATEGORY) 145 | 146 | this.setAnchorsRef = this.setAnchorsRef.bind(this) 147 | this.handleAnchorClick = this.handleAnchorClick.bind(this) 148 | this.setSearchRef = this.setSearchRef.bind(this) 149 | this.handleSearch = this.handleSearch.bind(this) 150 | this.setScrollRef = this.setScrollRef.bind(this) 151 | this.handleScroll = this.handleScroll.bind(this) 152 | this.handleScrollPaint = this.handleScrollPaint.bind(this) 153 | this.handleEmojiOver = this.handleEmojiOver.bind(this) 154 | this.handleEmojiLeave = this.handleEmojiLeave.bind(this) 155 | this.handleEmojiClick = this.handleEmojiClick.bind(this) 156 | this.handleEmojiSelect = this.handleEmojiSelect.bind(this) 157 | this.setPreviewRef = this.setPreviewRef.bind(this) 158 | this.handleSkinChange = this.handleSkinChange.bind(this) 159 | this.handleKeyDown = this.handleKeyDown.bind(this) 160 | } 161 | 162 | componentWillReceiveProps(props) { 163 | if (props.skin) { 164 | this.setState({ skin: props.skin }) 165 | } else if (props.defaultSkin && !store.get('skin')) { 166 | this.setState({ skin: props.defaultSkin }) 167 | } 168 | } 169 | 170 | componentDidMount() { 171 | if (this.state.firstRender) { 172 | this.testStickyPosition() 173 | this.firstRenderTimeout = setTimeout(() => { 174 | this.setState({ firstRender: false }) 175 | }, 60) 176 | } 177 | } 178 | 179 | componentDidUpdate() { 180 | this.updateCategoriesSize() 181 | this.handleScroll() 182 | } 183 | 184 | componentWillUnmount() { 185 | this.SEARCH_CATEGORY.emojis = null 186 | 187 | clearTimeout(this.leaveTimeout) 188 | clearTimeout(this.firstRenderTimeout) 189 | } 190 | 191 | testStickyPosition() { 192 | const stickyTestElement = document.createElement('div') 193 | 194 | const prefixes = ['', '-webkit-', '-ms-', '-moz-', '-o-'] 195 | 196 | prefixes.forEach( 197 | (prefix) => (stickyTestElement.style.position = `${prefix}sticky`), 198 | ) 199 | 200 | this.hasStickyPosition = !!stickyTestElement.style.position.length 201 | } 202 | 203 | handleEmojiOver(emoji) { 204 | var { preview } = this 205 | if (!preview) { 206 | return 207 | } 208 | 209 | // Use Array.prototype.find() when it is more widely supported. 210 | const emojiData = this.CUSTOM_CATEGORY.emojis.filter( 211 | (customEmoji) => customEmoji.id === emoji.id, 212 | )[0] 213 | for (let key in emojiData) { 214 | if (emojiData.hasOwnProperty(key)) { 215 | emoji[key] = emojiData[key] 216 | } 217 | } 218 | 219 | preview.setState({ emoji }) 220 | clearTimeout(this.leaveTimeout) 221 | } 222 | 223 | handleEmojiLeave(emoji) { 224 | var { preview } = this 225 | if (!preview) { 226 | return 227 | } 228 | 229 | this.leaveTimeout = setTimeout(() => { 230 | preview.setState({ emoji: null }) 231 | }, 16) 232 | } 233 | 234 | handleEmojiClick(emoji, e) { 235 | this.props.onClick(emoji, e) 236 | this.handleEmojiSelect(emoji) 237 | } 238 | 239 | handleEmojiSelect(emoji) { 240 | this.props.onSelect(emoji) 241 | if (!this.hideRecent && !this.props.recent) frequently.add(emoji) 242 | 243 | var component = this.categoryRefs['category-1'] 244 | if (component) { 245 | let maxMargin = component.maxMargin 246 | component.forceUpdate() 247 | 248 | window.requestAnimationFrame(() => { 249 | if (!this.scroll) return 250 | component.memoizeSize() 251 | if (maxMargin == component.maxMargin) return 252 | 253 | this.updateCategoriesSize() 254 | this.handleScrollPaint() 255 | 256 | if (this.SEARCH_CATEGORY.emojis) { 257 | component.updateDisplay('none') 258 | } 259 | }) 260 | } 261 | } 262 | 263 | handleScroll() { 264 | if (!this.waitingForPaint) { 265 | this.waitingForPaint = true 266 | window.requestAnimationFrame(this.handleScrollPaint) 267 | } 268 | } 269 | 270 | handleScrollPaint() { 271 | this.waitingForPaint = false 272 | 273 | if (!this.scroll) { 274 | return 275 | } 276 | 277 | let activeCategory = null 278 | 279 | if (this.SEARCH_CATEGORY.emojis) { 280 | activeCategory = this.SEARCH_CATEGORY 281 | } else { 282 | var target = this.scroll, 283 | scrollTop = target.scrollTop, 284 | scrollingDown = scrollTop > (this.scrollTop || 0), 285 | minTop = 0 286 | 287 | for (let i = 0, l = this.categories.length; i < l; i++) { 288 | let ii = scrollingDown ? this.categories.length - 1 - i : i, 289 | category = this.categories[ii], 290 | component = this.categoryRefs[`category-${ii}`] 291 | 292 | if (component) { 293 | let active = component.handleScroll(scrollTop) 294 | 295 | if (!minTop || component.top < minTop) { 296 | if (component.top > 0) { 297 | minTop = component.top 298 | } 299 | } 300 | 301 | if (active && !activeCategory) { 302 | activeCategory = category 303 | } 304 | } 305 | } 306 | 307 | if (scrollTop < minTop) { 308 | activeCategory = this.categories.filter( 309 | (category) => !(category.anchor === false), 310 | )[0] 311 | } else if (scrollTop + this.clientHeight >= this.scrollHeight) { 312 | activeCategory = this.categories[this.categories.length - 1] 313 | } 314 | } 315 | 316 | if (activeCategory) { 317 | let { anchors } = this, 318 | { name: categoryName } = activeCategory 319 | 320 | if (anchors.state.selected != categoryName) { 321 | anchors.setState({ selected: categoryName }) 322 | } 323 | } 324 | 325 | this.scrollTop = scrollTop 326 | } 327 | 328 | handleSearch(emojis) { 329 | this.SEARCH_CATEGORY.emojis = emojis 330 | 331 | for (let i = 0, l = this.categories.length; i < l; i++) { 332 | let component = this.categoryRefs[`category-${i}`] 333 | 334 | if (component && component.props.name != 'Search') { 335 | let display = emojis ? 'none' : 'inherit' 336 | component.updateDisplay(display) 337 | } 338 | } 339 | 340 | this.forceUpdate() 341 | this.scroll.scrollTop = 0 342 | this.handleScroll() 343 | } 344 | 345 | handleAnchorClick(category, i) { 346 | var component = this.categoryRefs[`category-${i}`], 347 | { scroll, anchors } = this, 348 | scrollToComponent = null 349 | 350 | scrollToComponent = () => { 351 | if (component) { 352 | let { top } = component 353 | 354 | if (category.first) { 355 | top = 0 356 | } else { 357 | top += 1 358 | } 359 | 360 | scroll.scrollTop = top 361 | } 362 | } 363 | 364 | if (this.SEARCH_CATEGORY.emojis) { 365 | this.handleSearch(null) 366 | this.search.clear() 367 | 368 | window.requestAnimationFrame(scrollToComponent) 369 | } else { 370 | scrollToComponent() 371 | } 372 | } 373 | 374 | handleSkinChange(skin) { 375 | var newState = { skin: skin }, 376 | { onSkinChange } = this.props 377 | 378 | this.setState(newState) 379 | store.update(newState) 380 | 381 | onSkinChange(skin) 382 | } 383 | 384 | handleKeyDown(e) { 385 | let handled = false 386 | 387 | switch (e.keyCode) { 388 | case 13: 389 | let emoji 390 | 391 | if ( 392 | this.SEARCH_CATEGORY.emojis && 393 | (emoji = this.SEARCH_CATEGORY.emojis[0]) 394 | ) { 395 | this.handleEmojiSelect(emoji) 396 | } 397 | 398 | handled = true 399 | break 400 | } 401 | 402 | if (handled) { 403 | e.preventDefault() 404 | } 405 | } 406 | 407 | updateCategoriesSize() { 408 | for (let i = 0, l = this.categories.length; i < l; i++) { 409 | let component = this.categoryRefs[`category-${i}`] 410 | if (component) component.memoizeSize() 411 | } 412 | 413 | if (this.scroll) { 414 | let target = this.scroll 415 | this.scrollHeight = target.scrollHeight 416 | this.clientHeight = target.clientHeight 417 | } 418 | } 419 | 420 | getCategories() { 421 | return this.state.firstRender 422 | ? this.categories.slice(0, 3) 423 | : this.categories 424 | } 425 | 426 | setAnchorsRef(c) { 427 | this.anchors = c 428 | } 429 | 430 | setSearchRef(c) { 431 | this.search = c 432 | } 433 | 434 | setPreviewRef(c) { 435 | this.preview = c 436 | } 437 | 438 | setScrollRef(c) { 439 | this.scroll = c 440 | } 441 | 442 | setCategoryRef(name, c) { 443 | if (!this.categoryRefs) { 444 | this.categoryRefs = {} 445 | } 446 | 447 | this.categoryRefs[name] = c 448 | } 449 | 450 | render() { 451 | var { 452 | perLine, 453 | emojiSize, 454 | set, 455 | sheetSize, 456 | style, 457 | title, 458 | emoji, 459 | color, 460 | native, 461 | backgroundImageFn, 462 | emojisToShowFilter, 463 | showPreview, 464 | showSkinTones, 465 | emojiTooltip, 466 | include, 467 | exclude, 468 | recent, 469 | autoFocus, 470 | } = this.props, 471 | { skin } = this.state, 472 | width = perLine * (emojiSize + 12) + 12 + 2 + measureScrollbar() 473 | 474 | return ( 475 |
480 |
481 | 489 |
490 | 491 | 502 | 503 |
508 | {this.getCategories().map((category, i) => { 509 | return ( 510 | 543 | ) 544 | })} 545 |
546 | 547 | {showPreview && ( 548 |
549 | 568 |
569 | )} 570 |
571 | ) 572 | } 573 | } 574 | 575 | NimblePicker.propTypes = { 576 | ...PickerPropTypes, 577 | data: PropTypes.object.isRequired, 578 | } 579 | NimblePicker.defaultProps = { ...PickerDefaultProps } 580 | -------------------------------------------------------------------------------- /src/components/picker/nimblePicker.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 305 | 306 | 319 | 320 | 361 | -------------------------------------------------------------------------------- /src/components/picker/picker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import data from '../../../data/all.json' 4 | import NimblePicker from './nimble-picker' 5 | 6 | import { PickerPropTypes, PickerDefaultProps } from '../../utils/shared-props' 7 | 8 | export default class Picker extends React.PureComponent { 9 | render() { 10 | return 11 | } 12 | } 13 | 14 | Picker.propTypes = PickerPropTypes 15 | Picker.defaultProps = { ...PickerDefaultProps, data } 16 | -------------------------------------------------------------------------------- /src/components/picker/picker.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/components/preview.old.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { getData } from '../utils' 5 | import { NimbleEmoji, Skins } from '.' 6 | 7 | export default class Preview extends React.PureComponent { 8 | constructor(props) { 9 | super(props) 10 | 11 | this.data = props.data 12 | this.state = { emoji: null } 13 | } 14 | 15 | render() { 16 | var { emoji } = this.state, 17 | { 18 | emojiProps, 19 | skinsProps, 20 | showSkinTones, 21 | title, 22 | emoji: idleEmoji, 23 | } = this.props 24 | 25 | if (emoji) { 26 | var emojiData = getData(emoji, null, null, this.data), 27 | { emoticons = [] } = emojiData, 28 | knownEmoticons = [], 29 | listedEmoticons = [] 30 | 31 | emoticons.forEach((emoticon) => { 32 | if (knownEmoticons.indexOf(emoticon.toLowerCase()) >= 0) { 33 | return 34 | } 35 | 36 | knownEmoticons.push(emoticon.toLowerCase()) 37 | listedEmoticons.push(emoticon) 38 | }) 39 | 40 | return ( 41 |
42 |
43 | {NimbleEmoji({ 44 | key: emoji.id, 45 | emoji: emoji, 46 | data: this.data, 47 | ...emojiProps, 48 | })} 49 |
50 | 51 |
52 |
{emoji.name}
53 |
54 | {emojiData.short_names.map((short_name) => ( 55 | 56 | :{short_name}: 57 | 58 | ))} 59 |
60 |
61 | {listedEmoticons.map((emoticon) => ( 62 | 63 | {emoticon} 64 | 65 | ))} 66 |
67 |
68 |
69 | ) 70 | } else { 71 | return ( 72 |
73 |
74 | {idleEmoji && 75 | idleEmoji.length && 76 | NimbleEmoji({ emoji: idleEmoji, data: this.data, ...emojiProps })} 77 |
78 | 79 |
80 | {title} 81 |
82 | 83 | {showSkinTones && ( 84 |
85 | 86 |
87 | )} 88 |
89 | ) 90 | } 91 | } 92 | } 93 | 94 | Preview.propTypes = { 95 | showSkinTones: PropTypes.bool, 96 | title: PropTypes.string.isRequired, 97 | emoji: PropTypes.string.isRequired, 98 | emojiProps: PropTypes.object.isRequired, 99 | skinsProps: PropTypes.object.isRequired, 100 | } 101 | 102 | Preview.defaultProps = { 103 | showSkinTones: true, 104 | onChange: () => {}, 105 | } 106 | -------------------------------------------------------------------------------- /src/components/preview.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 117 | 118 | 182 | -------------------------------------------------------------------------------- /src/components/search.old.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import NimbleEmojiIndex from '../utils/emoji-index/nimble-emoji-index' 5 | 6 | export default class Search extends React.PureComponent { 7 | constructor(props) { 8 | super(props) 9 | 10 | this.data = props.data 11 | this.emojiIndex = new NimbleEmojiIndex(this.data) 12 | this.setRef = this.setRef.bind(this) 13 | this.handleChange = this.handleChange.bind(this) 14 | } 15 | 16 | handleChange() { 17 | var value = this.input.value 18 | 19 | this.props.onSearch( 20 | this.emojiIndex.search(value, { 21 | emojisToShowFilter: this.props.emojisToShowFilter, 22 | maxResults: this.props.maxResults, 23 | include: this.props.include, 24 | exclude: this.props.exclude, 25 | custom: this.props.custom, 26 | }), 27 | ) 28 | } 29 | 30 | setRef(c) { 31 | this.input = c 32 | } 33 | 34 | clear() { 35 | this.input.value = '' 36 | } 37 | 38 | render() { 39 | var { i18n, autoFocus } = this.props 40 | 41 | return ( 42 |
43 | 50 |
51 | ) 52 | } 53 | } 54 | 55 | Search.propTypes = { 56 | onSearch: PropTypes.func, 57 | maxResults: PropTypes.number, 58 | emojisToShowFilter: PropTypes.func, 59 | autoFocus: PropTypes.bool, 60 | } 61 | 62 | Search.defaultProps = { 63 | onSearch: () => {}, 64 | maxResults: 75, 65 | emojisToShowFilter: null, 66 | autoFocus: false, 67 | } 68 | -------------------------------------------------------------------------------- /src/components/search.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 82 | 83 | 101 | -------------------------------------------------------------------------------- /src/components/skins.old.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class Skins extends React.PureComponent { 5 | constructor(props) { 6 | super(props) 7 | 8 | this.state = { 9 | opened: false, 10 | } 11 | 12 | this.handleClick = this.handleClick.bind(this) 13 | } 14 | 15 | handleClick(e) { 16 | var skin = parseInt(e.currentTarget.getAttribute('data-skin')) 17 | var { onChange } = this.props 18 | 19 | if (!this.state.opened) { 20 | this.setState({ opened: true }) 21 | } else { 22 | this.setState({ opened: false }) 23 | if (skin != this.props.skin) { 24 | onChange(skin) 25 | } 26 | } 27 | } 28 | 29 | render() { 30 | const { skin } = this.props 31 | const { opened } = this.state 32 | 33 | const skinToneNodes = [] 34 | 35 | for (let i = 0; i < 6; i++) { 36 | const skinTone = i + 1 37 | const selected = skinTone == skin 38 | 39 | skinToneNodes.push( 40 | 46 | 51 | , 52 | ) 53 | } 54 | 55 | return ( 56 |
57 |
62 | {skinToneNodes} 63 |
64 |
65 | ) 66 | } 67 | } 68 | 69 | Skins.propTypes = { 70 | onChange: PropTypes.func, 71 | skin: PropTypes.number.isRequired, 72 | } 73 | 74 | Skins.defaultProps = { 75 | onChange: () => {}, 76 | } 77 | -------------------------------------------------------------------------------- /src/components/skins.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import emojiIndex from './utils/emoji-index/emoji-index' 2 | import store from './utils/store' 3 | import frequently from './utils/frequently' 4 | 5 | export { 6 | Picker, 7 | NimblePicker, 8 | Emoji, 9 | NimbleEmoji, 10 | Category, 11 | } from './components' 12 | 13 | export { default as NimbleEmojiIndex } from './utils/emoji-index/nimble-emoji-index' 14 | export { emojiIndex, store, frequently } 15 | -------------------------------------------------------------------------------- /src/polyfills/createClass.js: -------------------------------------------------------------------------------- 1 | const _Object = Object 2 | 3 | export default (function createClass() { 4 | function defineProperties(target, props) { 5 | for (var i = 0; i < props.length; i++) { 6 | var descriptor = props[i] 7 | descriptor.enumerable = descriptor.enumerable || false 8 | descriptor.configurable = true 9 | if ('value' in descriptor) descriptor.writable = true 10 | _Object.defineProperty(target, descriptor.key, descriptor) 11 | } 12 | } 13 | 14 | return function(Constructor, protoProps, staticProps) { 15 | if (protoProps) defineProperties(Constructor.prototype, protoProps) 16 | if (staticProps) defineProperties(Constructor, staticProps) 17 | return Constructor 18 | } 19 | })() 20 | -------------------------------------------------------------------------------- /src/polyfills/extends.js: -------------------------------------------------------------------------------- 1 | const _Object = Object 2 | 3 | export default _Object.assign || 4 | function(target) { 5 | for (var i = 1; i < arguments.length; i++) { 6 | var source = arguments[i] 7 | 8 | for (var key in source) { 9 | if (Object.prototype.hasOwnProperty.call(source, key)) { 10 | target[key] = source[key] 11 | } 12 | } 13 | } 14 | 15 | return target 16 | } 17 | -------------------------------------------------------------------------------- /src/polyfills/inherits.js: -------------------------------------------------------------------------------- 1 | const _Object = Object 2 | 3 | export default function inherits(subClass, superClass) { 4 | if (typeof superClass !== 'function' && superClass !== null) { 5 | throw new TypeError( 6 | 'Super expression must either be null or a function, not ' + 7 | typeof superClass, 8 | ) 9 | } 10 | 11 | subClass.prototype = _Object.create(superClass && superClass.prototype, { 12 | constructor: { 13 | value: subClass, 14 | enumerable: false, 15 | writable: true, 16 | configurable: true, 17 | }, 18 | }) 19 | if (superClass) { 20 | _Object.setPrototypeOf 21 | ? _Object.setPrototypeOf(subClass, superClass) 22 | : (subClass.__proto__ = superClass) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/polyfills/objectGetPrototypeOf.js: -------------------------------------------------------------------------------- 1 | const _Object = Object 2 | 3 | export default _Object.getPrototypeOf || 4 | function(O) { 5 | O = Object(O) 6 | 7 | if (typeof O.constructor === 'function' && O instanceof O.constructor) { 8 | return O.constructor.prototype 9 | } 10 | 11 | return O instanceof Object ? Object.prototype : null 12 | } 13 | -------------------------------------------------------------------------------- /src/polyfills/possibleConstructorReturn.js: -------------------------------------------------------------------------------- 1 | export default function possibleConstructorReturn(self, call) { 2 | if (!self) { 3 | throw new ReferenceError( 4 | "this hasn't been initialised - super() hasn't been called", 5 | ) 6 | } 7 | 8 | return call && (typeof call === 'object' || typeof call === 'function') 9 | ? call 10 | : self 11 | } 12 | -------------------------------------------------------------------------------- /src/polyfills/stringFromCodePoint.js: -------------------------------------------------------------------------------- 1 | const _String = String 2 | 3 | export default _String.fromCodePoint || 4 | function stringFromCodePoint() { 5 | var MAX_SIZE = 0x4000 6 | var codeUnits = [] 7 | var highSurrogate 8 | var lowSurrogate 9 | var index = -1 10 | var length = arguments.length 11 | if (!length) { 12 | return '' 13 | } 14 | var result = '' 15 | while (++index < length) { 16 | var codePoint = Number(arguments[index]) 17 | if ( 18 | !isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity` 19 | codePoint < 0 || // not a valid Unicode code point 20 | codePoint > 0x10ffff || // not a valid Unicode code point 21 | Math.floor(codePoint) != codePoint // not an integer 22 | ) { 23 | throw RangeError('Invalid code point: ' + codePoint) 24 | } 25 | if (codePoint <= 0xffff) { 26 | // BMP code point 27 | codeUnits.push(codePoint) 28 | } else { 29 | // Astral code point; split in surrogate halves 30 | // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae 31 | codePoint -= 0x10000 32 | highSurrogate = (codePoint >> 10) + 0xd800 33 | lowSurrogate = codePoint % 0x400 + 0xdc00 34 | codeUnits.push(highSurrogate, lowSurrogate) 35 | } 36 | if (index + 1 === length || codeUnits.length > MAX_SIZE) { 37 | result += String.fromCharCode.apply(null, codeUnits) 38 | codeUnits.length = 0 39 | } 40 | } 41 | return result 42 | } 43 | -------------------------------------------------------------------------------- /src/svgs/index.js: -------------------------------------------------------------------------------- 1 | const SVGs = { 2 | activity: ``, 3 | 4 | custom: ``, 5 | 6 | flags: ``, 7 | 8 | foods: ``, 9 | 10 | nature: ``, 11 | 12 | objects: ``, 13 | 14 | people: ``, 15 | 16 | places: ``, 17 | 18 | recent: ``, 19 | 20 | symbols: ``, 21 | } 22 | 23 | export default SVGs 24 | -------------------------------------------------------------------------------- /src/utils/data.js: -------------------------------------------------------------------------------- 1 | const mapping = { 2 | name: 'a', 3 | unified: 'b', 4 | non_qualified: 'c', 5 | has_img_apple: 'd', 6 | has_img_google: 'e', 7 | has_img_twitter: 'f', 8 | has_img_emojione: 'g', 9 | has_img_facebook: 'h', 10 | has_img_messenger: 'i', 11 | keywords: 'j', 12 | sheet: 'k', 13 | emoticons: 'l', 14 | text: 'm', 15 | short_names: 'n', 16 | added_in: 'o', 17 | } 18 | 19 | const buildSearch = (emoji) => { 20 | const search = [] 21 | 22 | var addToSearch = (strings, split) => { 23 | if (!strings) { 24 | return 25 | } 26 | 27 | ;(Array.isArray(strings) ? strings : [strings]).forEach((string) => { 28 | ;(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => { 29 | s = s.toLowerCase() 30 | 31 | if (search.indexOf(s) == -1) { 32 | search.push(s) 33 | } 34 | }) 35 | }) 36 | } 37 | 38 | addToSearch(emoji.short_names, true) 39 | addToSearch(emoji.name, true) 40 | addToSearch(emoji.keywords, false) 41 | addToSearch(emoji.emoticons, false) 42 | 43 | return search.join(',') 44 | } 45 | 46 | const compress = (emoji) => { 47 | emoji.short_names = emoji.short_names.filter((short_name) => { 48 | return short_name !== emoji.short_name 49 | }) 50 | delete emoji.short_name 51 | 52 | emoji.sheet = [emoji.sheet_x, emoji.sheet_y] 53 | delete emoji.sheet_x 54 | delete emoji.sheet_y 55 | 56 | emoji.added_in = parseInt(emoji.added_in) 57 | if (emoji.added_in === 6) { 58 | delete emoji.added_in 59 | } 60 | 61 | for (let key in mapping) { 62 | emoji[mapping[key]] = emoji[key] 63 | delete emoji[key] 64 | } 65 | 66 | for (let key in emoji) { 67 | let value = emoji[key] 68 | 69 | if (Array.isArray(value) && !value.length) { 70 | delete emoji[key] 71 | } else if (typeof value === 'string' && !value.length) { 72 | delete emoji[key] 73 | } else if (value === null) { 74 | delete emoji[key] 75 | } 76 | } 77 | } 78 | 79 | const uncompress = (data) => { 80 | data.compressed = false 81 | 82 | for (let id in data.emojis) { 83 | let emoji = data.emojis[id] 84 | 85 | for (let key in mapping) { 86 | emoji[key] = emoji[mapping[key]] 87 | delete emoji[mapping[key]] 88 | } 89 | 90 | if (!emoji.short_names) emoji.short_names = [] 91 | emoji.short_names.unshift(id) 92 | 93 | emoji.sheet_x = emoji.sheet[0] 94 | emoji.sheet_y = emoji.sheet[1] 95 | delete emoji.sheet 96 | 97 | if (!emoji.text) emoji.text = '' 98 | 99 | if (!emoji.added_in) emoji.added_in = 6 100 | emoji.added_in = emoji.added_in.toFixed(1) 101 | 102 | emoji.search = buildSearch(emoji) 103 | } 104 | } 105 | 106 | module.exports = { buildSearch, compress, uncompress } 107 | -------------------------------------------------------------------------------- /src/utils/emoji-index/emoji-index.js: -------------------------------------------------------------------------------- 1 | import data from '../../../data/all.json' 2 | import NimbleEmojiIndex from './nimble-emoji-index' 3 | 4 | const emojiIndex = new NimbleEmojiIndex(data) 5 | const { emojis, emoticons } = emojiIndex 6 | 7 | function search() { 8 | return emojiIndex.search(...arguments) 9 | } 10 | 11 | export default { search, emojis, emoticons } 12 | -------------------------------------------------------------------------------- /src/utils/emoji-index/nimble-emoji-index.js: -------------------------------------------------------------------------------- 1 | import { getData, getSanitizedData, intersect } from '..' 2 | import { uncompress } from '../data' 3 | 4 | export default class NimbleEmojiIndex { 5 | constructor(data) { 6 | if (data.compressed) { 7 | data = uncompress(data) 8 | } 9 | 10 | this.data = data || {} 11 | this.originalPool = {} 12 | this.index = {} 13 | this.emojis = {} 14 | this.emoticons = {} 15 | this.customEmojisList = [] 16 | 17 | this.buildIndex() 18 | } 19 | 20 | buildIndex() { 21 | for (let emoji in this.data.emojis) { 22 | let emojiData = this.data.emojis[emoji], 23 | { short_names, emoticons } = emojiData, 24 | id = short_names[0] 25 | 26 | if (emoticons) { 27 | emoticons.forEach((emoticon) => { 28 | if (this.emoticons[emoticon]) { 29 | return 30 | } 31 | 32 | this.emoticons[emoticon] = id 33 | }) 34 | } 35 | 36 | this.emojis[id] = getSanitizedData(id, null, null, this.data) 37 | this.originalPool[id] = emojiData 38 | } 39 | } 40 | 41 | clearCustomEmojis(pool) { 42 | this.customEmojisList.forEach((emoji) => { 43 | let emojiId = emoji.id || emoji.short_names[0] 44 | 45 | delete pool[emojiId] 46 | delete this.emojis[emojiId] 47 | }) 48 | } 49 | 50 | addCustomToPool(custom, pool) { 51 | if (this.customEmojisList.length) this.clearCustomEmojis(pool) 52 | 53 | custom.forEach((emoji) => { 54 | let emojiId = emoji.id || emoji.short_names[0] 55 | 56 | if (emojiId && !pool[emojiId]) { 57 | pool[emojiId] = getData(emoji, null, null, this.data) 58 | this.emojis[emojiId] = getSanitizedData(emoji, null, null, this.data) 59 | } 60 | }) 61 | 62 | this.customEmojisList = custom 63 | this.index = {} 64 | } 65 | 66 | search( 67 | value, 68 | { emojisToShowFilter, maxResults, include, exclude, custom = [] } = {}, 69 | ) { 70 | if (this.customEmojisList != custom) 71 | this.addCustomToPool(custom, this.originalPool) 72 | 73 | maxResults || (maxResults = 75) 74 | include || (include = []) 75 | exclude || (exclude = []) 76 | 77 | var results = null, 78 | pool = this.originalPool 79 | 80 | if (value.length) { 81 | if (value == '-' || value == '-1') { 82 | return [this.emojis['-1']] 83 | } 84 | 85 | var values = value.toLowerCase().split(/[\s|,|\-|_]+/), 86 | allResults = [] 87 | 88 | if (values.length > 2) { 89 | values = [values[0], values[1]] 90 | } 91 | 92 | if (include.length || exclude.length) { 93 | pool = {} 94 | 95 | this.data.categories.forEach((category) => { 96 | let isIncluded = 97 | include && include.length ? include.indexOf(category.id) > -1 : true 98 | let isExcluded = 99 | exclude && exclude.length 100 | ? exclude.indexOf(category.id) > -1 101 | : false 102 | if (!isIncluded || isExcluded) { 103 | return 104 | } 105 | 106 | category.emojis.forEach( 107 | (emojiId) => (pool[emojiId] = this.data.emojis[emojiId]), 108 | ) 109 | }) 110 | 111 | if (custom.length) { 112 | let customIsIncluded = 113 | include && include.length ? include.indexOf('custom') > -1 : true 114 | let customIsExcluded = 115 | exclude && exclude.length ? exclude.indexOf('custom') > -1 : false 116 | if (customIsIncluded && !customIsExcluded) { 117 | this.addCustomToPool(custom, pool) 118 | } 119 | } 120 | } 121 | 122 | allResults = values 123 | .map((value) => { 124 | var aPool = pool, 125 | aIndex = this.index, 126 | length = 0 127 | 128 | for (let charIndex = 0; charIndex < value.length; charIndex++) { 129 | const char = value[charIndex] 130 | length++ 131 | 132 | aIndex[char] || (aIndex[char] = {}) 133 | aIndex = aIndex[char] 134 | 135 | if (!aIndex.results) { 136 | let scores = {} 137 | 138 | aIndex.results = [] 139 | aIndex.pool = {} 140 | 141 | for (let id in aPool) { 142 | let emoji = aPool[id], 143 | { search } = emoji, 144 | sub = value.substr(0, length), 145 | subIndex = search.indexOf(sub) 146 | 147 | if (subIndex != -1) { 148 | let score = subIndex + 1 149 | if (sub == id) score = 0 150 | 151 | aIndex.results.push(this.emojis[id]) 152 | aIndex.pool[id] = emoji 153 | 154 | scores[id] = score 155 | } 156 | } 157 | 158 | aIndex.results.sort((a, b) => { 159 | var aScore = scores[a.id], 160 | bScore = scores[b.id] 161 | 162 | return aScore - bScore 163 | }) 164 | } 165 | 166 | aPool = aIndex.pool 167 | } 168 | 169 | return aIndex.results 170 | }) 171 | .filter((a) => a) 172 | 173 | if (allResults.length > 1) { 174 | results = intersect.apply(null, allResults) 175 | } else if (allResults.length) { 176 | results = allResults[0] 177 | } else { 178 | results = [] 179 | } 180 | } 181 | 182 | if (results) { 183 | if (emojisToShowFilter) { 184 | results = results.filter((result) => 185 | emojisToShowFilter(pool[result.id]), 186 | ) 187 | } 188 | 189 | if (results && results.length > maxResults) { 190 | results = results.slice(0, maxResults) 191 | } 192 | } 193 | 194 | return results 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/utils/frequently.js: -------------------------------------------------------------------------------- 1 | import store from './store' 2 | 3 | const DEFAULTS = [ 4 | '+1', 5 | 'grinning', 6 | 'kissing_heart', 7 | 'heart_eyes', 8 | 'laughing', 9 | 'stuck_out_tongue_winking_eye', 10 | 'sweat_smile', 11 | 'joy', 12 | 'scream', 13 | 'disappointed', 14 | 'unamused', 15 | 'weary', 16 | 'sob', 17 | 'sunglasses', 18 | 'heart', 19 | 'poop', 20 | ] 21 | 22 | let frequently, initialized 23 | let defaults = {} 24 | 25 | function init() { 26 | initialized = true 27 | frequently = store.get('frequently') 28 | } 29 | 30 | function add(emoji) { 31 | if (!initialized) init() 32 | var { id } = emoji 33 | 34 | frequently || (frequently = defaults) 35 | frequently[id] || (frequently[id] = 0) 36 | frequently[id] += 1 37 | 38 | store.set('last', id) 39 | store.set('frequently', frequently) 40 | } 41 | 42 | function get(perLine) { 43 | if (!initialized) init() 44 | if (!frequently) { 45 | defaults = {} 46 | 47 | const result = [] 48 | 49 | for (let i = 0; i < perLine; i++) { 50 | defaults[DEFAULTS[i]] = perLine - i 51 | result.push(DEFAULTS[i]) 52 | } 53 | 54 | return result 55 | } 56 | 57 | const quantity = perLine * 4 58 | const frequentlyKeys = [] 59 | 60 | for (let key in frequently) { 61 | if (frequently.hasOwnProperty(key)) { 62 | frequentlyKeys.push(key) 63 | } 64 | } 65 | 66 | const sorted = frequentlyKeys 67 | .sort((a, b) => frequently[a] - frequently[b]) 68 | .reverse() 69 | const sliced = sorted.slice(0, quantity) 70 | 71 | const last = store.get('last') 72 | 73 | if (last && sliced.indexOf(last) == -1) { 74 | sliced.pop() 75 | sliced.push(last) 76 | } 77 | 78 | return sliced 79 | } 80 | 81 | export default { add, get } 82 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { buildSearch } from './data' 2 | import stringFromCodePoint from '../polyfills/stringFromCodePoint' 3 | 4 | const _JSON = JSON 5 | 6 | const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/ 7 | const SKINS = ['1F3FA', '1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'] 8 | 9 | function unifiedToNative(unified) { 10 | var unicodes = unified.split('-'), 11 | codePoints = unicodes.map((u) => `0x${u}`) 12 | 13 | return stringFromCodePoint.apply(null, codePoints) 14 | } 15 | 16 | function sanitize(emoji) { 17 | var { 18 | name, 19 | short_names, 20 | skin_tone, 21 | skin_variations, 22 | emoticons, 23 | unified, 24 | custom, 25 | imageUrl, 26 | } = emoji, 27 | id = emoji.id || short_names[0], 28 | colons = `:${id}:` 29 | 30 | if (custom) { 31 | return { 32 | id, 33 | name, 34 | colons, 35 | emoticons, 36 | custom, 37 | imageUrl, 38 | } 39 | } 40 | 41 | if (skin_tone) { 42 | colons += `:skin-tone-${skin_tone}:` 43 | } 44 | 45 | return { 46 | id, 47 | name, 48 | colons, 49 | emoticons, 50 | unified: unified.toLowerCase(), 51 | skin: skin_tone || (skin_variations ? 1 : null), 52 | native: unifiedToNative(unified), 53 | } 54 | } 55 | 56 | function getSanitizedData() { 57 | return sanitize(getData(...arguments)) 58 | } 59 | 60 | function cloneEmoji(emoji) { 61 | if (typeof emoji === 'string') { 62 | return emoji; 63 | } 64 | 65 | return Object.assign({}, emoji); 66 | } 67 | 68 | function getData(_emoji, skin, set, data) { 69 | var emoji = cloneEmoji(_emoji) 70 | var emojiData = {} 71 | 72 | if (typeof emoji == 'string') { 73 | let matches = emoji.match(COLONS_REGEX) 74 | 75 | if (matches) { 76 | emoji = matches[1] 77 | 78 | if (matches[2]) { 79 | skin = parseInt(matches[2], 10) 80 | } 81 | } 82 | 83 | if (data.aliases.hasOwnProperty(emoji)) { 84 | emoji = data.aliases[emoji] 85 | } 86 | 87 | if (data.emojis.hasOwnProperty(emoji)) { 88 | emojiData = data.emojis[emoji] 89 | } else { 90 | return null 91 | } 92 | } else if (emoji.id) { 93 | if (data.aliases.hasOwnProperty(emoji.id)) { 94 | emoji.id = data.aliases[emoji.id] 95 | } 96 | 97 | if (data.emojis.hasOwnProperty(emoji.id)) { 98 | emojiData = data.emojis[emoji.id] 99 | skin || (skin = emoji.skin) 100 | } 101 | } 102 | 103 | if (!Object.keys(emojiData).length) { 104 | emojiData = emoji 105 | emojiData.custom = true 106 | 107 | if (!emojiData.search) { 108 | emojiData.search = buildSearch(emoji) 109 | } 110 | } 111 | 112 | emojiData.emoticons || (emojiData.emoticons = []) 113 | emojiData.variations || (emojiData.variations = []) 114 | 115 | if (emojiData.skin_variations && skin > 1) { 116 | emojiData = JSON.parse(_JSON.stringify(emojiData)) 117 | 118 | var skinKey = SKINS[skin - 1], 119 | variationData = emojiData.skin_variations[skinKey] 120 | 121 | if (!variationData.variations && emojiData.variations) { 122 | delete emojiData.variations 123 | } 124 | 125 | if ( 126 | set == 'native' || 127 | variationData[`has_img_${set}`] == undefined || 128 | variationData[`has_img_${set}`] 129 | ) { 130 | emojiData.skin_tone = skin 131 | 132 | for (let k in variationData) { 133 | let v = variationData[k] 134 | emojiData[k] = v 135 | } 136 | } 137 | } 138 | 139 | if (emojiData.variations && emojiData.variations.length) { 140 | emojiData = JSON.parse(_JSON.stringify(emojiData)) 141 | emojiData.unified = emojiData.variations.shift() 142 | } 143 | 144 | return emojiData 145 | } 146 | 147 | function uniq(arr) { 148 | return arr.reduce((acc, item) => { 149 | if (acc.indexOf(item) === -1) { 150 | acc.push(item) 151 | } 152 | return acc 153 | }, []) 154 | } 155 | 156 | function intersect(a, b) { 157 | const uniqA = uniq(a) 158 | const uniqB = uniq(b) 159 | 160 | return uniqA.filter((item) => uniqB.indexOf(item) >= 0) 161 | } 162 | 163 | function deepMerge(a, b) { 164 | var o = {} 165 | 166 | for (let key in a) { 167 | let originalValue = a[key], 168 | value = originalValue 169 | 170 | if (b.hasOwnProperty(key)) { 171 | value = b[key] 172 | } 173 | 174 | if (typeof value === 'object') { 175 | value = deepMerge(originalValue, value) 176 | } 177 | 178 | o[key] = value 179 | } 180 | 181 | return o 182 | } 183 | 184 | // https://github.com/sonicdoe/measure-scrollbar 185 | function measureScrollbar() { 186 | if (typeof document == 'undefined') return 0 187 | const div = document.createElement('div') 188 | 189 | div.style.width = '100px' 190 | div.style.height = '100px' 191 | div.style.overflow = 'scroll' 192 | div.style.position = 'absolute' 193 | div.style.top = '-9999px' 194 | 195 | document.body.appendChild(div) 196 | const scrollbarWidth = div.offsetWidth - div.clientWidth 197 | document.body.removeChild(div) 198 | 199 | return scrollbarWidth 200 | } 201 | 202 | export { 203 | getData, 204 | getSanitizedData, 205 | uniq, 206 | intersect, 207 | deepMerge, 208 | unifiedToNative, 209 | measureScrollbar, 210 | } 211 | -------------------------------------------------------------------------------- /src/utils/shared-props.js: -------------------------------------------------------------------------------- 1 | const EmojiProps = { 2 | backgroundImageFn: { 3 | type: Function, 4 | default: function(set, sheetSize) { 5 | return `https://unpkg.com/emoji-datasource-${set}@${EMOJI_DATASOURCE_VERSION}/img/${set}/sheets-256/${sheetSize}.png` 6 | } 7 | }, 8 | native: { 9 | type: Boolean, 10 | default: false 11 | }, 12 | forceSize: { 13 | type: Boolean, 14 | default: false 15 | }, 16 | tooltip: { 17 | type: Boolean, 18 | default: false 19 | }, 20 | fallback: { 21 | type: Function 22 | }, 23 | skin: { 24 | type: Number, 25 | default: 1 26 | }, 27 | sheetSize: { 28 | type: Number, 29 | default: 64 30 | }, 31 | set: { 32 | type: String, 33 | default: 'apple' 34 | }, 35 | size: { 36 | type: Number, 37 | default: 24 38 | }, 39 | emoji: { 40 | type: [String, Object], 41 | required: true 42 | } 43 | } 44 | 45 | const PickerProps = { 46 | perLine: { 47 | type: Number, 48 | default: 9 49 | }, 50 | emojiSize: { 51 | type: Number, 52 | default: 24 53 | }, 54 | title: { 55 | type: String, 56 | default: 'Emoji Mart™' 57 | }, 58 | emoji: { 59 | type: String, 60 | default: 'department_store' 61 | }, 62 | color: { 63 | type: String, 64 | default: '#ae65c5' 65 | }, 66 | set: { 67 | type: String, 68 | default: 'apple' 69 | }, 70 | skin: { 71 | type: Number, 72 | default: null 73 | }, 74 | defaultSkin: { 75 | type: Number, 76 | default: 1 77 | }, 78 | native: { 79 | type: Boolean, 80 | default: false 81 | }, 82 | backgroundImageFn: { 83 | type: Function 84 | }, 85 | sheetSize: { 86 | type: Number, 87 | default: 64 88 | }, 89 | emojisToShowFilter: { 90 | type: Function 91 | }, 92 | emojiTooltip: { 93 | type: Boolean, 94 | default: false 95 | }, 96 | include: { 97 | type: Array 98 | }, 99 | exclude: { 100 | type: Array 101 | }, 102 | recent: { 103 | type: Array 104 | }, 105 | autoFocus: { 106 | type: Boolean, 107 | default: false 108 | }, 109 | custom: { 110 | type: Array, 111 | default() { 112 | return [] 113 | } 114 | }, 115 | i18n: { 116 | type: Object, 117 | default() { 118 | return {} 119 | } 120 | }, 121 | showPreview: { 122 | type: Boolean, 123 | default: true 124 | }, 125 | showSearch: { 126 | type: Boolean, 127 | default: true 128 | }, 129 | showCategories: { 130 | type: Boolean, 131 | default: true 132 | }, 133 | showSkinTones: { 134 | type: Boolean, 135 | default: true 136 | }, 137 | infiniteScroll: { 138 | type: Boolean, 139 | default: true 140 | }, 141 | pickerStyles: { 142 | type: Object, 143 | default() { 144 | return {} 145 | } 146 | } 147 | } 148 | 149 | export { 150 | EmojiProps, 151 | PickerProps 152 | } 153 | -------------------------------------------------------------------------------- /src/utils/store.js: -------------------------------------------------------------------------------- 1 | var NAMESPACE = 'emoji-mart' 2 | 3 | const _JSON = JSON 4 | 5 | var isLocalStorageSupported = 6 | typeof window !== 'undefined' && 'localStorage' in window 7 | 8 | let getter 9 | let setter 10 | 11 | function setHandlers(handlers) { 12 | handlers || (handlers = {}) 13 | 14 | getter = handlers.getter 15 | setter = handlers.setter 16 | } 17 | 18 | function setNamespace(namespace) { 19 | NAMESPACE = namespace 20 | } 21 | 22 | function update(state) { 23 | for (let key in state) { 24 | let value = state[key] 25 | set(key, value) 26 | } 27 | } 28 | 29 | function set(key, value) { 30 | if (setter) { 31 | setter(key, value) 32 | } else { 33 | if (!isLocalStorageSupported) return 34 | try { 35 | window.localStorage[`${NAMESPACE}.${key}`] = _JSON.stringify(value) 36 | } catch (e) {} 37 | } 38 | } 39 | 40 | function get(key) { 41 | if (getter) { 42 | return getter(key) 43 | } else { 44 | if (!isLocalStorageSupported) return 45 | try { 46 | var value = window.localStorage[`${NAMESPACE}.${key}`] 47 | } catch (e) { 48 | return 49 | } 50 | 51 | if (value) { 52 | return JSON.parse(value) 53 | } 54 | } 55 | } 56 | 57 | export default { update, set, get, setNamespace, setHandlers } 58 | -------------------------------------------------------------------------------- /src/vendor/raf-polyfill.js: -------------------------------------------------------------------------------- 1 | // http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 2 | // http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 3 | 4 | // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel 5 | 6 | // MIT license 7 | 8 | var isWindowAvailable = typeof window !== 'undefined' 9 | 10 | isWindowAvailable && 11 | (function() { 12 | var lastTime = 0 13 | var vendors = ['ms', 'moz', 'webkit', 'o'] 14 | 15 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 16 | window.requestAnimationFrame = 17 | window[vendors[x] + 'RequestAnimationFrame'] 18 | window.cancelAnimationFrame = 19 | window[vendors[x] + 'CancelAnimationFrame'] || 20 | window[vendors[x] + 'CancelRequestAnimationFrame'] 21 | } 22 | 23 | if (!window.requestAnimationFrame) 24 | window.requestAnimationFrame = function(callback, element) { 25 | var currTime = new Date().getTime() 26 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)) 27 | var id = window.setTimeout(function() { 28 | callback(currTime + timeToCall) 29 | }, timeToCall) 30 | 31 | lastTime = currTime + timeToCall 32 | return id 33 | } 34 | 35 | if (!window.cancelAnimationFrame) 36 | window.cancelAnimationFrame = function(id) { 37 | clearTimeout(id) 38 | } 39 | })() 40 | -------------------------------------------------------------------------------- /src/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var pack = require('../package.json') 3 | var webpack = require('webpack') 4 | 5 | var PROD = process.env.NODE_ENV === 'production'; 6 | var TEST = process.env.NODE_ENV === 'test'; 7 | 8 | module.exports = { 9 | entry: path.resolve('src/index.js'), 10 | output: { 11 | path: path.resolve('dist'), 12 | filename: 'emoji-mart.js', 13 | library: 'EmojiMart', 14 | libraryTarget: 'umd', 15 | }, 16 | 17 | externals: !TEST && [{ 18 | 'vue': { 19 | root: 'Vue', 20 | commonjs2: 'vue', 21 | commonjs: 'vue', 22 | amd: 'vue', 23 | }, 24 | }], 25 | 26 | module: { 27 | loaders: [ 28 | { 29 | test: /\.js$/, 30 | loader: 'babel-loader', 31 | include: [ 32 | path.resolve('src'), 33 | path.resolve('data'), 34 | ], 35 | }, 36 | { 37 | test: /\.vue$/, 38 | loader: 'vue-loader', 39 | include: [ 40 | path.resolve('src'), 41 | ] 42 | }, 43 | { 44 | test: /\.css$/, 45 | use: ["vue-style-loader", "css-loader", "postcss-loader"] 46 | }, 47 | { 48 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 49 | loader: 'url-loader', 50 | options: { 51 | limit: 10000 52 | } 53 | } 54 | ], 55 | }, 56 | 57 | resolve: { 58 | extensions: ['.vue', '.js'], 59 | }, 60 | 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | EMOJI_DATASOURCE_VERSION: `'${pack.devDependencies['emoji-datasource']}'`, 64 | }), 65 | ], 66 | 67 | bail: true, 68 | } 69 | --------------------------------------------------------------------------------