logo

pleroma-fe

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

emoji_picker.js (11471B)


  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: true
  91. },
  92. hideCustomEmoji: {
  93. required: false,
  94. type: Boolean,
  95. default: false
  96. }
  97. },
  98. inject: {
  99. popoversZLayer: {
  100. default: ''
  101. }
  102. },
  103. data () {
  104. return {
  105. keyword: '',
  106. activeGroup: 'custom',
  107. showingStickers: false,
  108. groupsScrolledClass: 'scrolled-top',
  109. keepOpen: false,
  110. customEmojiTimeout: null,
  111. hideCustomEmojiInPicker: false,
  112. // Lazy-load only after the first time `showing` becomes true.
  113. contentLoaded: false,
  114. groupRefs: {},
  115. emojiRefs: {},
  116. filteredEmojiGroups: [],
  117. emojiSize: 0,
  118. width: 0
  119. }
  120. },
  121. components: {
  122. StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
  123. Checkbox,
  124. StillImage,
  125. Popover
  126. },
  127. methods: {
  128. groupScroll (e) {
  129. e.currentTarget.scrollLeft += e.deltaY + e.deltaX
  130. },
  131. updateEmojiSize () {
  132. const css = window.getComputedStyle(this.$refs.popover.$el)
  133. const fontSize = css.getPropertyValue('font-size') || '14px'
  134. const emojiSize = css.getPropertyValue('--emojiSize') || '2.2rem'
  135. const fontSizeUnit = fontSize.replace(/[0-9,.]+/, '')
  136. const fontSizeValue = Number(fontSize.replace(/[^0-9,.]+/, ''))
  137. const emojiSizeUnit = emojiSize.replace(/[0-9,.]+/, '')
  138. const emojiSizeValue = Number(emojiSize.replace(/[^0-9,.]+/, ''))
  139. let fontSizeMultiplier
  140. if (fontSizeUnit.endsWith('em')) {
  141. fontSizeMultiplier = fontSizeValue
  142. } else {
  143. fontSizeMultiplier = fontSizeValue / 14
  144. }
  145. let emojiSizeReal
  146. if (emojiSizeUnit.endsWith('em')) {
  147. emojiSizeReal = emojiSizeValue * fontSizeMultiplier * 14
  148. } else {
  149. emojiSizeReal = emojiSizeValue
  150. }
  151. const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSizeMultiplier * 14)
  152. this.emojiSize = fullEmojiSize
  153. },
  154. showPicker () {
  155. this.$refs.popover.showPopover()
  156. this.$nextTick(() => {
  157. this.onShowing()
  158. })
  159. },
  160. hidePicker () {
  161. this.$refs.popover.hidePopover()
  162. },
  163. setAnchorEl (el) {
  164. this.$refs.popover.setAnchorEl(el)
  165. },
  166. setGroupRef (name) {
  167. return el => { this.groupRefs[name] = el }
  168. },
  169. onPopoverShown () {
  170. this.$emit('show')
  171. },
  172. onPopoverClosed () {
  173. this.$emit('close')
  174. },
  175. onStickerUploaded (e) {
  176. this.$emit('sticker-uploaded', e)
  177. },
  178. onStickerUploadFailed (e) {
  179. this.$emit('sticker-upload-failed', e)
  180. },
  181. onEmoji (emoji) {
  182. const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
  183. if (!this.keepOpen) {
  184. this.$refs.popover.hidePopover()
  185. }
  186. this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen })
  187. },
  188. onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
  189. const target = this.$refs['emoji-groups'].$el
  190. this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
  191. },
  192. scrolledGroup (target, start, end) {
  193. const top = target.scrollTop + 5
  194. this.$nextTick(() => {
  195. this.emojiItems.slice(start, end + 1).forEach(group => {
  196. const headerId = toHeaderId(group.id)
  197. const ref = this.groupRefs['group-' + group.id]
  198. if (!ref) { return }
  199. const elem = ref.$el.parentElement
  200. if (!elem) { return }
  201. if (elem && getOffset(elem) <= top) {
  202. this.activeGroup = headerId
  203. }
  204. })
  205. this.scrollHeader()
  206. })
  207. },
  208. scrollHeader () {
  209. // Scroll the active tab's header into view
  210. const headerRef = this.groupRefs['group-header-' + this.activeGroup]
  211. const left = headerRef.offsetLeft
  212. const right = left + headerRef.offsetWidth
  213. const headerCont = this.$refs.header
  214. const currentScroll = headerCont.scrollLeft
  215. const currentScrollRight = currentScroll + headerCont.clientWidth
  216. const setScroll = s => { headerCont.scrollLeft = s }
  217. const margin = 7 // .emoji-tabs-item: padding
  218. if (left - margin < currentScroll) {
  219. setScroll(left - margin)
  220. } else if (right + margin > currentScrollRight) {
  221. setScroll(right + margin - headerCont.clientWidth)
  222. }
  223. },
  224. highlight (groupId) {
  225. this.setShowStickers(false)
  226. const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
  227. this.$refs['emoji-groups'].scrollToItem(indexInList)
  228. },
  229. updateScrolledClass (target) {
  230. if (target.scrollTop <= 5) {
  231. this.groupsScrolledClass = 'scrolled-top'
  232. } else if (target.scrollTop >= target.scrollTopMax - 5) {
  233. this.groupsScrolledClass = 'scrolled-bottom'
  234. } else {
  235. this.groupsScrolledClass = 'scrolled-middle'
  236. }
  237. },
  238. toggleStickers () {
  239. this.showingStickers = !this.showingStickers
  240. },
  241. setShowStickers (value) {
  242. this.showingStickers = value
  243. },
  244. filterByKeyword (list, keyword) {
  245. return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
  246. },
  247. onShowing () {
  248. const oldContentLoaded = this.contentLoaded
  249. this.updateEmojiSize()
  250. this.recalculateItemPerRow()
  251. this.$nextTick(() => {
  252. this.$refs.search.focus()
  253. })
  254. this.contentLoaded = true
  255. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  256. if (!oldContentLoaded) {
  257. this.$nextTick(() => {
  258. if (this.defaultGroup) {
  259. this.highlight(this.defaultGroup)
  260. }
  261. })
  262. }
  263. },
  264. getFilteredEmojiGroups () {
  265. return this.allEmojiGroups
  266. .map(group => ({
  267. ...group,
  268. emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
  269. }))
  270. .filter(group => group.emojis.length > 0)
  271. },
  272. recalculateItemPerRow () {
  273. this.$nextTick(() => {
  274. if (!this.$refs['emoji-groups']) {
  275. return
  276. }
  277. this.width = this.$refs['emoji-groups'].$el.clientWidth
  278. })
  279. }
  280. },
  281. watch: {
  282. keyword () {
  283. this.onScroll()
  284. this.debouncedHandleKeywordChange()
  285. },
  286. allCustomGroups () {
  287. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  288. }
  289. },
  290. computed: {
  291. minItemSize () {
  292. return this.emojiSize
  293. },
  294. // used to watch it
  295. fontSize () {
  296. this.$nextTick(() => {
  297. this.updateEmojiSize()
  298. })
  299. return this.$store.getters.mergedConfig.fontSize
  300. },
  301. emojiHeight () {
  302. return this.emojiSize
  303. },
  304. itemPerRow () {
  305. return this.width ? Math.floor(this.width / this.emojiSize) : 6
  306. },
  307. activeGroupView () {
  308. return this.showingStickers ? '' : this.activeGroup
  309. },
  310. stickersAvailable () {
  311. if (this.$store.state.instance.stickers) {
  312. return this.$store.state.instance.stickers.length > 0
  313. }
  314. return 0
  315. },
  316. allCustomGroups () {
  317. if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
  318. return {}
  319. }
  320. const emojis = this.$store.getters.groupedCustomEmojis
  321. if (emojis.unpacked) {
  322. emojis.unpacked.text = this.$t('emoji.unpacked')
  323. }
  324. return emojis
  325. },
  326. defaultGroup () {
  327. return Object.keys(this.allCustomGroups)[0]
  328. },
  329. unicodeEmojiGroups () {
  330. return this.$store.getters.standardEmojiGroupList.map(group => ({
  331. id: `standard-${group.id}`,
  332. text: this.$t(`emoji.unicode_groups.${group.id}`),
  333. icon: UNICODE_EMOJI_GROUP_ICON[group.id],
  334. emojis: group.emojis
  335. }))
  336. },
  337. allEmojiGroups () {
  338. return Object.entries(this.allCustomGroups)
  339. .map(([, v]) => v)
  340. .concat(this.unicodeEmojiGroups)
  341. },
  342. stickerPickerEnabled () {
  343. return (this.$store.state.instance.stickers || []).length !== 0
  344. },
  345. debouncedHandleKeywordChange () {
  346. return debounce(() => {
  347. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  348. }, 500)
  349. },
  350. emojiItems () {
  351. return this.filteredEmojiGroups.map(group =>
  352. chunk(group.emojis, this.itemPerRow)
  353. .map((items, index) => ({
  354. ...group,
  355. id: index === 0 ? group.id : `row-${index}-${group.id}`,
  356. emojis: items,
  357. isFirstRow: index === 0
  358. })))
  359. .reduce((a, c) => a.concat(c), [])
  360. },
  361. languages () {
  362. return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
  363. },
  364. maybeLocalizedEmojiName () {
  365. return emoji => {
  366. if (!emoji.annotations) {
  367. return emoji.displayText
  368. }
  369. if (emoji.displayTextI18n) {
  370. return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
  371. }
  372. for (const lang of this.languages) {
  373. if (emoji.annotations[lang]?.name) {
  374. return emoji.annotations[lang].name
  375. }
  376. }
  377. return emoji.displayText
  378. }
  379. },
  380. isInModal () {
  381. return this.popoversZLayer === 'modals'
  382. }
  383. }
  384. }
  385. export default EmojiPicker