logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git

emoji_picker.js (10981B)


  1. import { defineAsyncComponent } from 'vue'
  2. import Checkbox from '../checkbox/checkbox.vue'
  3. import Popover from 'src/components/popover/popover.vue'
  4. import StillImage from '../still-image/still-image.vue'
  5. import { ensureFinalFallback } from '../../i18n/languages.js'
  6. import { library } from '@fortawesome/fontawesome-svg-core'
  7. import {
  8. faBoxOpen,
  9. faStickyNote,
  10. faSmileBeam,
  11. faSmile,
  12. faUser,
  13. faPaw,
  14. faIceCream,
  15. faBus,
  16. faBasketballBall,
  17. faLightbulb,
  18. faCode,
  19. faFlag
  20. } from '@fortawesome/free-solid-svg-icons'
  21. import { debounce, trim, chunk } from 'lodash'
  22. library.add(
  23. faBoxOpen,
  24. faStickyNote,
  25. faSmileBeam,
  26. faSmile,
  27. faUser,
  28. faPaw,
  29. faIceCream,
  30. faBus,
  31. faBasketballBall,
  32. faLightbulb,
  33. faCode,
  34. faFlag
  35. )
  36. const UNICODE_EMOJI_GROUP_ICON = {
  37. 'smileys-and-emotion': 'smile',
  38. 'people-and-body': 'user',
  39. 'animals-and-nature': 'paw',
  40. 'food-and-drink': 'ice-cream',
  41. 'travel-and-places': 'bus',
  42. activities: 'basketball-ball',
  43. objects: 'lightbulb',
  44. symbols: 'code',
  45. flags: 'flag'
  46. }
  47. const maybeLocalizedKeywords = (emoji, languages, nameLocalizer) => {
  48. const res = [emoji.displayText, nameLocalizer(emoji)]
  49. if (emoji.annotations) {
  50. languages.forEach(lang => {
  51. const keywords = emoji.annotations[lang]?.keywords || []
  52. const name = emoji.annotations[lang]?.name
  53. res.push(...(keywords.concat([name]).filter(k => k)))
  54. })
  55. }
  56. return res
  57. }
  58. const filterByKeyword = (list, keyword = '', languages, nameLocalizer) => {
  59. if (keyword === '') return list
  60. const keywordLowercase = keyword.toLowerCase()
  61. const orderedEmojiList = []
  62. for (const emoji of list) {
  63. const indices = maybeLocalizedKeywords(emoji, languages, nameLocalizer)
  64. .map(k => k.toLowerCase().indexOf(keywordLowercase))
  65. .filter(k => k > -1)
  66. const indexOfKeyword = indices.length ? Math.min(...indices) : -1
  67. if (indexOfKeyword > -1) {
  68. if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
  69. orderedEmojiList[indexOfKeyword] = []
  70. }
  71. orderedEmojiList[indexOfKeyword].push(emoji)
  72. }
  73. }
  74. return orderedEmojiList.flat()
  75. }
  76. const getOffset = (elem) => {
  77. const style = elem.style.transform
  78. const res = /translateY\((\d+)px\)/.exec(style)
  79. if (!res) { return 0 }
  80. return res[1]
  81. }
  82. const toHeaderId = id => {
  83. return id.replace(/^row-\d+-/, '')
  84. }
  85. const EmojiPicker = {
  86. props: {
  87. enableStickerPicker: {
  88. required: false,
  89. type: Boolean,
  90. default: false
  91. },
  92. hideCustomEmoji: {
  93. required: false,
  94. type: Boolean,
  95. default: false
  96. }
  97. },
  98. inject: ['popoversZLayer'],
  99. data () {
  100. return {
  101. keyword: '',
  102. activeGroup: 'custom',
  103. showingStickers: false,
  104. groupsScrolledClass: 'scrolled-top',
  105. keepOpen: false,
  106. customEmojiTimeout: null,
  107. hideCustomEmojiInPicker: false,
  108. // Lazy-load only after the first time `showing` becomes true.
  109. contentLoaded: false,
  110. groupRefs: {},
  111. emojiRefs: {},
  112. filteredEmojiGroups: [],
  113. emojiSize: 0,
  114. width: 0
  115. }
  116. },
  117. components: {
  118. StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
  119. Checkbox,
  120. StillImage,
  121. Popover
  122. },
  123. methods: {
  124. updateEmojiSize () {
  125. const css = window.getComputedStyle(this.$refs.popover.$el)
  126. const emojiSize = css.getPropertyValue('--emojiSize')
  127. const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
  128. const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
  129. const fontSize = css.getPropertyValue('font-size').replace(/[^0-9,.]+/, '')
  130. let emojiSizeReal
  131. if (emojiSizeUnit.endsWith('em')) {
  132. emojiSizeReal = emojiSizeValue * fontSize
  133. } else {
  134. emojiSizeReal = emojiSizeValue
  135. }
  136. const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSize)
  137. this.emojiSize = fullEmojiSize
  138. },
  139. showPicker () {
  140. this.$refs.popover.showPopover()
  141. this.$nextTick(() => {
  142. this.onShowing()
  143. })
  144. },
  145. hidePicker () {
  146. this.$refs.popover.hidePopover()
  147. },
  148. setAnchorEl (el) {
  149. this.$refs.popover.setAnchorEl(el)
  150. },
  151. setGroupRef (name) {
  152. return el => { this.groupRefs[name] = el }
  153. },
  154. onPopoverShown () {
  155. this.$emit('show')
  156. },
  157. onPopoverClosed () {
  158. this.$emit('close')
  159. },
  160. onStickerUploaded (e) {
  161. this.$emit('sticker-uploaded', e)
  162. },
  163. onStickerUploadFailed (e) {
  164. this.$emit('sticker-upload-failed', e)
  165. },
  166. onEmoji (emoji) {
  167. const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
  168. if (!this.keepOpen) {
  169. this.$refs.popover.hidePopover()
  170. }
  171. this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
  172. },
  173. onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
  174. const target = this.$refs['emoji-groups'].$el
  175. this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
  176. },
  177. scrolledGroup (target, start, end) {
  178. const top = target.scrollTop + 5
  179. this.$nextTick(() => {
  180. this.emojiItems.slice(start, end + 1).forEach(group => {
  181. const headerId = toHeaderId(group.id)
  182. const ref = this.groupRefs['group-' + group.id]
  183. if (!ref) { return }
  184. const elem = ref.$el.parentElement
  185. if (!elem) { return }
  186. if (elem && getOffset(elem) <= top) {
  187. this.activeGroup = headerId
  188. }
  189. })
  190. this.scrollHeader()
  191. })
  192. },
  193. scrollHeader () {
  194. // Scroll the active tab's header into view
  195. const headerRef = this.groupRefs['group-header-' + this.activeGroup]
  196. const left = headerRef.offsetLeft
  197. const right = left + headerRef.offsetWidth
  198. const headerCont = this.$refs.header
  199. const currentScroll = headerCont.scrollLeft
  200. const currentScrollRight = currentScroll + headerCont.clientWidth
  201. const setScroll = s => { headerCont.scrollLeft = s }
  202. const margin = 7 // .emoji-tabs-item: padding
  203. if (left - margin < currentScroll) {
  204. setScroll(left - margin)
  205. } else if (right + margin > currentScrollRight) {
  206. setScroll(right + margin - headerCont.clientWidth)
  207. }
  208. },
  209. highlight (groupId) {
  210. this.setShowStickers(false)
  211. const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
  212. this.$refs['emoji-groups'].scrollToItem(indexInList)
  213. },
  214. updateScrolledClass (target) {
  215. if (target.scrollTop <= 5) {
  216. this.groupsScrolledClass = 'scrolled-top'
  217. } else if (target.scrollTop >= target.scrollTopMax - 5) {
  218. this.groupsScrolledClass = 'scrolled-bottom'
  219. } else {
  220. this.groupsScrolledClass = 'scrolled-middle'
  221. }
  222. },
  223. toggleStickers () {
  224. this.showingStickers = !this.showingStickers
  225. },
  226. setShowStickers (value) {
  227. this.showingStickers = value
  228. },
  229. filterByKeyword (list, keyword) {
  230. return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
  231. },
  232. onShowing () {
  233. const oldContentLoaded = this.contentLoaded
  234. this.updateEmojiSize()
  235. this.recalculateItemPerRow()
  236. this.$nextTick(() => {
  237. this.$refs.search.focus()
  238. })
  239. this.contentLoaded = true
  240. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  241. if (!oldContentLoaded) {
  242. this.$nextTick(() => {
  243. if (this.defaultGroup) {
  244. this.highlight(this.defaultGroup)
  245. }
  246. })
  247. }
  248. },
  249. getFilteredEmojiGroups () {
  250. return this.allEmojiGroups
  251. .map(group => ({
  252. ...group,
  253. emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
  254. }))
  255. .filter(group => group.emojis.length > 0)
  256. },
  257. recalculateItemPerRow () {
  258. this.$nextTick(() => {
  259. if (!this.$refs['emoji-groups']) {
  260. return
  261. }
  262. this.width = this.$refs['emoji-groups'].$el.clientWidth
  263. })
  264. }
  265. },
  266. watch: {
  267. keyword () {
  268. this.onScroll()
  269. this.debouncedHandleKeywordChange()
  270. },
  271. allCustomGroups () {
  272. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  273. }
  274. },
  275. computed: {
  276. minItemSize () {
  277. return this.emojiSize
  278. },
  279. // used to watch it
  280. fontSize () {
  281. this.$nextTick(() => {
  282. this.updateEmojiSize()
  283. })
  284. return this.$store.getters.mergedConfig.fontSize
  285. },
  286. emojiHeight () {
  287. return this.emojiSize
  288. },
  289. itemPerRow () {
  290. return this.width ? Math.floor(this.width / this.emojiSize) : 6
  291. },
  292. activeGroupView () {
  293. return this.showingStickers ? '' : this.activeGroup
  294. },
  295. stickersAvailable () {
  296. if (this.$store.state.instance.stickers) {
  297. return this.$store.state.instance.stickers.length > 0
  298. }
  299. return 0
  300. },
  301. allCustomGroups () {
  302. if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
  303. return {}
  304. }
  305. const emojis = this.$store.getters.groupedCustomEmojis
  306. if (emojis.unpacked) {
  307. emojis.unpacked.text = this.$t('emoji.unpacked')
  308. }
  309. return emojis
  310. },
  311. defaultGroup () {
  312. return Object.keys(this.allCustomGroups)[0]
  313. },
  314. unicodeEmojiGroups () {
  315. return this.$store.getters.standardEmojiGroupList.map(group => ({
  316. id: `standard-${group.id}`,
  317. text: this.$t(`emoji.unicode_groups.${group.id}`),
  318. icon: UNICODE_EMOJI_GROUP_ICON[group.id],
  319. emojis: group.emojis
  320. }))
  321. },
  322. allEmojiGroups () {
  323. return Object.entries(this.allCustomGroups)
  324. .map(([_, v]) => v)
  325. .concat(this.unicodeEmojiGroups)
  326. },
  327. stickerPickerEnabled () {
  328. return (this.$store.state.instance.stickers || []).length !== 0
  329. },
  330. debouncedHandleKeywordChange () {
  331. return debounce(() => {
  332. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  333. }, 500)
  334. },
  335. emojiItems () {
  336. return this.filteredEmojiGroups.map(group =>
  337. chunk(group.emojis, this.itemPerRow)
  338. .map((items, index) => ({
  339. ...group,
  340. id: index === 0 ? group.id : `row-${index}-${group.id}`,
  341. emojis: items,
  342. isFirstRow: index === 0
  343. })))
  344. .reduce((a, c) => a.concat(c), [])
  345. },
  346. languages () {
  347. return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
  348. },
  349. maybeLocalizedEmojiName () {
  350. return emoji => {
  351. if (!emoji.annotations) {
  352. return emoji.displayText
  353. }
  354. if (emoji.displayTextI18n) {
  355. return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
  356. }
  357. for (const lang of this.languages) {
  358. if (emoji.annotations[lang]?.name) {
  359. return emoji.annotations[lang].name
  360. }
  361. }
  362. return emoji.displayText
  363. }
  364. },
  365. isInModal () {
  366. return this.popoversZLayer === 'modals'
  367. }
  368. }
  369. }
  370. export default EmojiPicker