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


  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. console.log(emojiSizeReal)
  152. const fullEmojiSize = emojiSizeReal + (2 * 0.2 * fontSizeMultiplier * 14)
  153. this.emojiSize = fullEmojiSize
  154. },
  155. showPicker () {
  156. this.$refs.popover.showPopover()
  157. this.$nextTick(() => {
  158. this.onShowing()
  159. })
  160. },
  161. hidePicker () {
  162. this.$refs.popover.hidePopover()
  163. },
  164. setAnchorEl (el) {
  165. this.$refs.popover.setAnchorEl(el)
  166. },
  167. setGroupRef (name) {
  168. return el => { this.groupRefs[name] = el }
  169. },
  170. onPopoverShown () {
  171. this.$emit('show')
  172. },
  173. onPopoverClosed () {
  174. this.$emit('close')
  175. },
  176. onStickerUploaded (e) {
  177. this.$emit('sticker-uploaded', e)
  178. },
  179. onStickerUploadFailed (e) {
  180. this.$emit('sticker-upload-failed', e)
  181. },
  182. onEmoji (emoji) {
  183. const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
  184. if (!this.keepOpen) {
  185. this.$refs.popover.hidePopover()
  186. }
  187. this.$emit('emoji', { insertion: value, insertionUrl: emoji.imageUrl, keepOpen: this.keepOpen })
  188. },
  189. onScroll (startIndex, endIndex, visibleStartIndex, visibleEndIndex) {
  190. const target = this.$refs['emoji-groups'].$el
  191. this.scrolledGroup(target, visibleStartIndex, visibleEndIndex)
  192. },
  193. scrolledGroup (target, start, end) {
  194. const top = target.scrollTop + 5
  195. this.$nextTick(() => {
  196. this.emojiItems.slice(start, end + 1).forEach(group => {
  197. const headerId = toHeaderId(group.id)
  198. const ref = this.groupRefs['group-' + group.id]
  199. if (!ref) { return }
  200. const elem = ref.$el.parentElement
  201. if (!elem) { return }
  202. if (elem && getOffset(elem) <= top) {
  203. this.activeGroup = headerId
  204. }
  205. })
  206. this.scrollHeader()
  207. })
  208. },
  209. scrollHeader () {
  210. // Scroll the active tab's header into view
  211. const headerRef = this.groupRefs['group-header-' + this.activeGroup]
  212. const left = headerRef.offsetLeft
  213. const right = left + headerRef.offsetWidth
  214. const headerCont = this.$refs.header
  215. const currentScroll = headerCont.scrollLeft
  216. const currentScrollRight = currentScroll + headerCont.clientWidth
  217. const setScroll = s => { headerCont.scrollLeft = s }
  218. const margin = 7 // .emoji-tabs-item: padding
  219. if (left - margin < currentScroll) {
  220. setScroll(left - margin)
  221. } else if (right + margin > currentScrollRight) {
  222. setScroll(right + margin - headerCont.clientWidth)
  223. }
  224. },
  225. highlight (groupId) {
  226. this.setShowStickers(false)
  227. const indexInList = this.emojiItems.findIndex(k => k.id === groupId)
  228. this.$refs['emoji-groups'].scrollToItem(indexInList)
  229. },
  230. updateScrolledClass (target) {
  231. if (target.scrollTop <= 5) {
  232. this.groupsScrolledClass = 'scrolled-top'
  233. } else if (target.scrollTop >= target.scrollTopMax - 5) {
  234. this.groupsScrolledClass = 'scrolled-bottom'
  235. } else {
  236. this.groupsScrolledClass = 'scrolled-middle'
  237. }
  238. },
  239. toggleStickers () {
  240. this.showingStickers = !this.showingStickers
  241. },
  242. setShowStickers (value) {
  243. this.showingStickers = value
  244. },
  245. filterByKeyword (list, keyword) {
  246. return filterByKeyword(list, keyword, this.languages, this.maybeLocalizedEmojiName)
  247. },
  248. onShowing () {
  249. const oldContentLoaded = this.contentLoaded
  250. this.updateEmojiSize()
  251. this.recalculateItemPerRow()
  252. this.$nextTick(() => {
  253. this.$refs.search.focus()
  254. })
  255. this.contentLoaded = true
  256. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  257. if (!oldContentLoaded) {
  258. this.$nextTick(() => {
  259. if (this.defaultGroup) {
  260. this.highlight(this.defaultGroup)
  261. }
  262. })
  263. }
  264. },
  265. getFilteredEmojiGroups () {
  266. return this.allEmojiGroups
  267. .map(group => ({
  268. ...group,
  269. emojis: this.filterByKeyword(group.emojis, trim(this.keyword))
  270. }))
  271. .filter(group => group.emojis.length > 0)
  272. },
  273. recalculateItemPerRow () {
  274. this.$nextTick(() => {
  275. if (!this.$refs['emoji-groups']) {
  276. return
  277. }
  278. this.width = this.$refs['emoji-groups'].$el.clientWidth
  279. })
  280. }
  281. },
  282. watch: {
  283. keyword () {
  284. this.onScroll()
  285. this.debouncedHandleKeywordChange()
  286. },
  287. allCustomGroups () {
  288. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  289. }
  290. },
  291. computed: {
  292. minItemSize () {
  293. return this.emojiSize
  294. },
  295. // used to watch it
  296. fontSize () {
  297. this.$nextTick(() => {
  298. this.updateEmojiSize()
  299. })
  300. return this.$store.getters.mergedConfig.fontSize
  301. },
  302. emojiHeight () {
  303. return this.emojiSize
  304. },
  305. itemPerRow () {
  306. console.log('CALC', this.emojiSize, this.width)
  307. return this.width ? Math.floor(this.width / this.emojiSize) : 6
  308. },
  309. activeGroupView () {
  310. return this.showingStickers ? '' : this.activeGroup
  311. },
  312. stickersAvailable () {
  313. if (this.$store.state.instance.stickers) {
  314. return this.$store.state.instance.stickers.length > 0
  315. }
  316. return 0
  317. },
  318. allCustomGroups () {
  319. if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
  320. return {}
  321. }
  322. const emojis = this.$store.getters.groupedCustomEmojis
  323. if (emojis.unpacked) {
  324. emojis.unpacked.text = this.$t('emoji.unpacked')
  325. }
  326. return emojis
  327. },
  328. defaultGroup () {
  329. return Object.keys(this.allCustomGroups)[0]
  330. },
  331. unicodeEmojiGroups () {
  332. return this.$store.getters.standardEmojiGroupList.map(group => ({
  333. id: `standard-${group.id}`,
  334. text: this.$t(`emoji.unicode_groups.${group.id}`),
  335. icon: UNICODE_EMOJI_GROUP_ICON[group.id],
  336. emojis: group.emojis
  337. }))
  338. },
  339. allEmojiGroups () {
  340. return Object.entries(this.allCustomGroups)
  341. .map(([_, v]) => v)
  342. .concat(this.unicodeEmojiGroups)
  343. },
  344. stickerPickerEnabled () {
  345. return (this.$store.state.instance.stickers || []).length !== 0
  346. },
  347. debouncedHandleKeywordChange () {
  348. return debounce(() => {
  349. this.filteredEmojiGroups = this.getFilteredEmojiGroups()
  350. }, 500)
  351. },
  352. emojiItems () {
  353. return this.filteredEmojiGroups.map(group =>
  354. chunk(group.emojis, this.itemPerRow)
  355. .map((items, index) => ({
  356. ...group,
  357. id: index === 0 ? group.id : `row-${index}-${group.id}`,
  358. emojis: items,
  359. isFirstRow: index === 0
  360. })))
  361. .reduce((a, c) => a.concat(c), [])
  362. },
  363. languages () {
  364. return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
  365. },
  366. maybeLocalizedEmojiName () {
  367. return emoji => {
  368. if (!emoji.annotations) {
  369. return emoji.displayText
  370. }
  371. if (emoji.displayTextI18n) {
  372. return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
  373. }
  374. for (const lang of this.languages) {
  375. if (emoji.annotations[lang]?.name) {
  376. return emoji.annotations[lang].name
  377. }
  378. }
  379. return emoji.displayText
  380. }
  381. },
  382. isInModal () {
  383. return this.popoversZLayer === 'modals'
  384. }
  385. }
  386. }
  387. export default EmojiPicker