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 (10109B)


  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. width: 0
  114. }
  115. },
  116. components: {
  117. StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
  118. Checkbox,
  119. StillImage,
  120. Popover
  121. },
  122. methods: {
  123. showPicker () {
  124. this.$refs.popover.showPopover()
  125. this.onShowing()
  126. },
  127. hidePicker () {
  128. this.$refs.popover.hidePopover()
  129. },
  130. setAnchorEl (el) {
  131. this.$refs.popover.setAnchorEl(el)
  132. },
  133. setGroupRef (name) {
  134. return el => { this.groupRefs[name] = el }
  135. },
  136. onPopoverShown () {
  137. this.$emit('show')
  138. },
  139. onPopoverClosed () {
  140. this.$emit('close')
  141. },
  142. onStickerUploaded (e) {
  143. this.$emit('sticker-uploaded', e)
  144. },
  145. onStickerUploadFailed (e) {
  146. this.$emit('sticker-upload-failed', e)
  147. },
  148. onEmoji (emoji) {
  149. const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
  150. if (!this.keepOpen) {
  151. this.$refs.popover.hidePopover()
  152. }
  153. this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
  154. },
  155. onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
  156. const target = this.$refs['emoji-groups'].$el
  157. this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
  158. },
  159. scrolledGroup (target, start, end) {
  160. const top = target.scrollTop + 5
  161. this.$nextTick(() => {
  162. this.emojiItems.slice(start, end + 1).forEach(group => {
  163. const headerId = toHeaderId(group.id)
  164. const ref = this.groupRefs['group-' + group.id]
  165. if (!ref) { return }
  166. const elem = ref.$el.parentElement
  167. if (!elem) { return }
  168. if (elem && getOffset(elem) <= top) {
  169. this.activeGroup = headerId
  170. }
  171. })
  172. this.scrollHeader()
  173. })
  174. },
  175. scrollHeader () {
  176. // Scroll the active tab's header into view
  177. const headerRef = this.groupRefs['group-header-' + this.activeGroup]
  178. const left = headerRef.offsetLeft
  179. const right = left + headerRef.offsetWidth
  180. const headerCont = this.$refs.header
  181. const currentScroll = headerCont.scrollLeft
  182. const currentScrollRight = currentScroll + headerCont.clientWidth
  183. const setScroll = s => { headerCont.scrollLeft = s }
  184. const margin = 7 // .emoji-tabs-item: padding
  185. if (left - margin < currentScroll) {
  186. setScroll(left - margin)
  187. } else if (right + margin > currentScrollRight) {
  188. setScroll(right + margin - headerCont.clientWidth)
  189. }
  190. },
  191. highlight (groupId) {
  192. this.setShowStickers(false)
  193. const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
  194. this.$refs['emoji-groups'].scrollToItem(indexInList)
  195. },
  196. updateScrolledClass (target) {
  197. if (target.scrollTop <= 5) {
  198. this.groupsScrolledClass = 'scrolled-top'
  199. } else if (target.scrollTop >= target.scrollTopMax - 5) {
  200. this.groupsScrolledClass = 'scrolled-bottom'
  201. } else {
  202. this.groupsScrolledClass = 'scrolled-middle'
  203. }
  204. },
  205. toggleStickers () {
  206. this.showingStickers = !this.showingStickers
  207. },
  208. setShowStickers (value) {
  209. this.showingStickers = value
  210. },
  211. filterByKeyword (list, keyword) {
  212. return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
  213. },
  214. onShowing () {
  215. const oldContentLoaded = this.contentLoaded
  216. this.recalculateItemPerRow()
  217. this.$nextTick(() => {
  218. this.$refs.search.focus()
  219. })
  220. this.contentLoaded = true
  221. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  222. if (!oldContentLoaded) {
  223. this.$nextTick(() => {
  224. if (this.defaultGroup) {
  225. this.highlight(this.defaultGroup)
  226. }
  227. })
  228. }
  229. },
  230. getFilteredEmojiGroups () {
  231. return this.allEmojiGroups
  232. .map(group => ({
  233. ...group,
  234. emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
  235. }))
  236. .filter(group => group.emojis.length > 0)
  237. },
  238. recalculateItemPerRow () {
  239. this.$nextTick(() => {
  240. if (!this.$refs['emoji-groups']) {
  241. return
  242. }
  243. this.width = this.$refs['emoji-groups'].$el.clientWidth
  244. })
  245. }
  246. },
  247. watch: {
  248. keyword () {
  249. this.onScroll()
  250. this.debouncedHandleKeywordChange()
  251. },
  252. allCustomGroups () {
  253. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  254. }
  255. },
  256. computed: {
  257. minItemSize () {
  258. return this.emojiHeight
  259. },
  260. emojiHeight () {
  261. return 32 + 4
  262. },
  263. emojiWidth () {
  264. return 32 + 4
  265. },
  266. itemPerRow () {
  267. return this.width ? Math.floor(this.width / this.emojiWidth - 1) : 6
  268. },
  269. activeGroupView () {
  270. return this.showingStickers ? '' : this.activeGroup
  271. },
  272. stickersAvailable () {
  273. if (this.$store.state.instance.stickers) {
  274. return this.$store.state.instance.stickers.length > 0
  275. }
  276. return 0
  277. },
  278. allCustomGroups () {
  279. if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
  280. return {}
  281. }
  282. const emojis = this.$store.getters.groupedCustomEmojis
  283. if (emojis.unpacked) {
  284. emojis.unpacked.text = this.$t('emoji.unpacked')
  285. }
  286. return emojis
  287. },
  288. defaultGroup () {
  289. return Object.keys(this.allCustomGroups)[0]
  290. },
  291. unicodeEmojiGroups () {
  292. return this.$store.getters.standardEmojiGroupList.map(group => ({
  293. id: `standard-${group.id}`,
  294. text: this.$t(`emoji.unicode_groups.${group.id}`),
  295. icon: UNICODE_EMOJI_GROUP_ICON[group.id],
  296. emojis: group.emojis
  297. }))
  298. },
  299. allEmojiGroups () {
  300. return Object.entries(this.allCustomGroups)
  301. .map(([_, v]) => v)
  302. .concat(this.unicodeEmojiGroups)
  303. },
  304. stickerPickerEnabled () {
  305. return (this.$store.state.instance.stickers || []).length !== 0
  306. },
  307. debouncedHandleKeywordChange () {
  308. return debounce(() => {
  309. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  310. }, 500)
  311. },
  312. emojiItems () {
  313. return this.filteredEmojiGroups.map(group =>
  314. chunk(group.emojis, this.itemPerRow)
  315. .map((items, index) => ({
  316. ...group,
  317. id: index === 0 ? group.id : `row-${index}-${group.id}`,
  318. emojis: items,
  319. isFirstRow: index === 0
  320. })))
  321. .reduce((a, c) => a.concat(c), [])
  322. },
  323. languages () {
  324. return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
  325. },
  326. maybeLocalizedEmojiName () {
  327. return emoji => {
  328. if (!emoji.annotations) {
  329. return emoji.displayText
  330. }
  331. if (emoji.displayTextI18n) {
  332. return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
  333. }
  334. for (const lang of this.languages) {
  335. if (emoji.annotations[lang]?.name) {
  336. return emoji.annotations[lang].name
  337. }
  338. }
  339. return emoji.displayText
  340. }
  341. },
  342. isInModal () {
  343. return this.popoversZLayer === 'modals'
  344. }
  345. }
  346. }
  347. export default EmojiPicker