logo

pleroma-fe

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

popover.js (13310B)


  1. const Popover = {
  2. name: 'Popover',
  3. props: {
  4. // Action to trigger popover: either 'hover' or 'click'
  5. trigger: String,
  6. // 'top', 'bottom', 'left', 'right'
  7. placement: String,
  8. // Takes object with properties 'x' and 'y', values of these can be
  9. // 'container' for using offsetParent as boundaries for either axis
  10. // or 'viewport'
  11. boundTo: Object,
  12. // Takes a selector to use as a replacement for the parent container
  13. // for getting boundaries for x an y axis
  14. boundToSelector: String,
  15. // Takes a top/bottom/left/right object, how much space to leave
  16. // between boundary and popover element
  17. margin: Object,
  18. // Takes a x/y object and tells how many pixels to offset from
  19. // anchor point on either axis
  20. offset: Object,
  21. // Replaces the classes you may want for the popover container.
  22. // Use 'popover-default' in addition to get the default popover
  23. // styles with your custom class.
  24. popoverClass: String,
  25. // If true, subtract padding when calculating position for the popover,
  26. // use it when popover offset looks to be different on top vs bottom.
  27. removePadding: Boolean,
  28. // self-explanatory (i hope)
  29. disabled: Boolean,
  30. // Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
  31. overlayCenters: Boolean,
  32. // What selector (witin popover!) to use for determining center of popover
  33. overlayCentersSelector: String,
  34. // Lets hover popover stay when clicking inside of it
  35. stayOnClick: Boolean,
  36. // Use styled button (to avoid nested buttons)
  37. normalButton: Boolean,
  38. triggerAttrs: {
  39. type: Object,
  40. default: {}
  41. }
  42. },
  43. inject: ['popoversZLayer'], // override popover z layer
  44. data () {
  45. return {
  46. // lockReEntry is a flag that is set when mouse cursor is leaving the popover's content
  47. // so that if mouse goes back into popover it won't be re-shown again to prevent annoyance
  48. // with popovers refusing to be hidden when user wants to interact with something in below popover
  49. anchorEl: null,
  50. // There's an issue where having teleport enabled by default causes things just...
  51. // not render at all, i.e. main post status form and its emoji inputs
  52. teleport: false,
  53. lockReEntry: false,
  54. hidden: true,
  55. styles: {},
  56. oldSize: { width: 0, height: 0 },
  57. scrollable: null,
  58. // used to avoid blinking if hovered onto popover
  59. graceTimeout: null,
  60. parentPopover: null,
  61. disableClickOutside: false,
  62. childrenShown: new Set()
  63. }
  64. },
  65. methods: {
  66. setAnchorEl (el) {
  67. this.anchorEl = el
  68. this.updateStyles()
  69. },
  70. containerBoundingClientRect () {
  71. const container = this.boundToSelector ? this.$el.closest(this.boundToSelector) : this.$el.offsetParent
  72. return container.getBoundingClientRect()
  73. },
  74. updateStyles () {
  75. if (this.hidden) {
  76. this.styles = {}
  77. return
  78. }
  79. // Popover will be anchored around this element, trigger ref is the container, so
  80. // its children are what are inside the slot. Expect only one v-slot:trigger.
  81. const anchorEl = this.anchorEl || (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
  82. // SVGs don't have offsetWidth/Height, use fallback
  83. const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
  84. const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
  85. const anchorScreenBox = anchorEl.getBoundingClientRect()
  86. const anchorStyle = getComputedStyle(anchorEl)
  87. const topPadding = parseFloat(anchorStyle.paddingTop)
  88. const bottomPadding = parseFloat(anchorStyle.paddingBottom)
  89. const rightPadding = parseFloat(anchorStyle.paddingRight)
  90. const leftPadding = parseFloat(anchorStyle.paddingLeft)
  91. // Screen position of the origin point for popover = center of the anchor
  92. const origin = {
  93. x: anchorScreenBox.left + anchorWidth * 0.5,
  94. y: anchorScreenBox.top + anchorHeight * 0.5
  95. }
  96. const content = this.$refs.content
  97. const overlayCenter = this.overlayCenters
  98. ? this.$refs.content.querySelector(this.overlayCentersSelector)
  99. : null
  100. // Minor optimization, don't call a slow reflow call if we don't have to
  101. const parentScreenBox = this.boundTo &&
  102. (this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
  103. this.containerBoundingClientRect()
  104. const margin = this.margin || {}
  105. // What are the screen bounds for the popover? Viewport vs container
  106. // when using viewport, using default margin values to dodge the navbar
  107. const xBounds = this.boundTo && this.boundTo.x === 'container'
  108. ? {
  109. min: parentScreenBox.left + (margin.left || 0),
  110. max: parentScreenBox.right - (margin.right || 0)
  111. }
  112. : {
  113. min: 0 + (margin.left || 10),
  114. max: window.innerWidth - (margin.right || 10)
  115. }
  116. const yBounds = this.boundTo && this.boundTo.y === 'container'
  117. ? {
  118. min: parentScreenBox.top + (margin.top || 0),
  119. max: parentScreenBox.bottom - (margin.bottom || 0)
  120. }
  121. : {
  122. min: 0 + (margin.top || 50),
  123. max: window.innerHeight - (margin.bottom || 5)
  124. }
  125. let horizOffset = 0
  126. let vertOffset = 0
  127. if (overlayCenter) {
  128. const box = content.getBoundingClientRect()
  129. const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
  130. const leftInnerOffset = overlayCenterScreenBox.left - box.left
  131. const topInnerOffset = overlayCenterScreenBox.top - box.top
  132. horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
  133. vertOffset = -topInnerOffset - overlayCenter.offsetHeight * 0.5
  134. } else {
  135. horizOffset = content.offsetWidth * -0.5
  136. vertOffset = content.offsetHeight * -0.5
  137. }
  138. const leftBorder = origin.x + horizOffset
  139. const rightBorder = leftBorder + content.offsetWidth
  140. const topBorder = origin.y + vertOffset
  141. const bottomBorder = topBorder + content.offsetHeight
  142. // If overflowing from left, move it so that it doesn't
  143. if (leftBorder < xBounds.min) {
  144. horizOffset += xBounds.min - leftBorder
  145. }
  146. // If overflowing from right, move it so that it doesn't
  147. if (rightBorder > xBounds.max) {
  148. horizOffset -= rightBorder - xBounds.max
  149. }
  150. // If overflowing from top, move it so that it doesn't
  151. if (topBorder < yBounds.min) {
  152. vertOffset += yBounds.min - topBorder
  153. }
  154. // If overflowing from bottom, move it so that it doesn't
  155. if (bottomBorder > yBounds.max) {
  156. vertOffset -= bottomBorder - yBounds.max
  157. }
  158. let translateX = 0
  159. let translateY = 0
  160. if (overlayCenter) {
  161. translateX = origin.x + horizOffset
  162. translateY = origin.y + vertOffset
  163. } else if (this.placement !== 'right' && this.placement !== 'left') {
  164. // Default to whatever user wished with placement prop
  165. let usingTop = this.placement !== 'bottom'
  166. // Handle special cases, first force to displaying on top if there's not space on bottom,
  167. // regardless of what placement value was. Then check if there's not space on top, and
  168. // force to bottom, again regardless of what placement value was.
  169. const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
  170. const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
  171. if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
  172. if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
  173. const yOffset = (this.offset && this.offset.y) || 0
  174. translateY = usingTop
  175. ? topBoundary - yOffset - content.offsetHeight
  176. : bottomBoundary + yOffset
  177. const xOffset = (this.offset && this.offset.x) || 0
  178. translateX = origin.x + horizOffset + xOffset
  179. } else {
  180. // Default to whatever user wished with placement prop
  181. let usingRight = this.placement !== 'left'
  182. // Handle special cases, first force to displaying on top if there's not space on bottom,
  183. // regardless of what placement value was. Then check if there's not space on top, and
  184. // force to bottom, again regardless of what placement value was.
  185. const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
  186. const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
  187. if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
  188. if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
  189. const xOffset = (this.offset && this.offset.x) || 0
  190. translateX = usingRight
  191. ? rightBoundary - xOffset - content.offsetWidth
  192. : leftBoundary + xOffset
  193. const yOffset = (this.offset && this.offset.y) || 0
  194. translateY = origin.y + vertOffset + yOffset
  195. }
  196. this.styles = {
  197. left: `${Math.round(translateX)}px`,
  198. top: `${Math.round(translateY)}px`
  199. }
  200. if (this.popoversZLayer) {
  201. this.styles['--ZI_popover_override'] = `var(--ZI_${this.popoversZLayer}_popovers)`
  202. }
  203. if (parentScreenBox) {
  204. this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
  205. }
  206. },
  207. showPopover () {
  208. if (this.disabled) return
  209. this.disableClickOutside = true
  210. setTimeout(() => {
  211. this.disableClickOutside = false
  212. }, 0)
  213. const wasHidden = this.hidden
  214. this.hidden = false
  215. this.parentPopover && this.parentPopover.onChildPopoverState(this, true)
  216. if (this.trigger === 'click' || this.stayOnClick) {
  217. document.addEventListener('click', this.onClickOutside)
  218. }
  219. this.scrollable.addEventListener('scroll', this.onScroll)
  220. this.scrollable.addEventListener('resize', this.onResize)
  221. this.$nextTick(() => {
  222. if (wasHidden) this.$emit('show')
  223. this.updateStyles()
  224. })
  225. },
  226. hidePopover () {
  227. if (this.disabled) return
  228. if (!this.hidden) this.$emit('close')
  229. this.hidden = true
  230. this.parentPopover && this.parentPopover.onChildPopoverState(this, false)
  231. if (this.trigger === 'click') {
  232. document.removeEventListener('click', this.onClickOutside)
  233. }
  234. this.scrollable.removeEventListener('scroll', this.onScroll)
  235. this.scrollable.removeEventListener('resize', this.onResize)
  236. },
  237. onMouseenter (e) {
  238. if (this.trigger === 'hover') {
  239. this.lockReEntry = false
  240. clearTimeout(this.graceTimeout)
  241. this.graceTimeout = null
  242. this.showPopover()
  243. }
  244. },
  245. onMouseleave (e) {
  246. if (this.trigger === 'hover' && this.childrenShown.size === 0) {
  247. this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
  248. }
  249. },
  250. onMouseenterContent (e) {
  251. if (this.trigger === 'hover' && !this.lockReEntry) {
  252. this.lockReEntry = true
  253. clearTimeout(this.graceTimeout)
  254. this.graceTimeout = null
  255. this.showPopover()
  256. }
  257. },
  258. onMouseleaveContent (e) {
  259. if (this.trigger === 'hover' && this.childrenShown.size === 0) {
  260. this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
  261. }
  262. },
  263. onClick (e) {
  264. if (this.trigger === 'click') {
  265. if (this.hidden) {
  266. this.showPopover()
  267. } else {
  268. this.hidePopover()
  269. }
  270. }
  271. },
  272. onClickOutside (e) {
  273. if (this.disableClickOutside) return
  274. if (this.hidden) return
  275. if (this.$refs.content && this.$refs.content.contains(e.target)) return
  276. if (this.$el.contains(e.target)) return
  277. if (this.childrenShown.size > 0) return
  278. this.hidePopover()
  279. if (this.parentPopover) this.parentPopover.onClickOutside(e)
  280. },
  281. onScroll (e) {
  282. this.updateStyles()
  283. },
  284. onResize (e) {
  285. this.updateStyles()
  286. },
  287. onChildPopoverState (childRef, state) {
  288. if (state) {
  289. this.childrenShown.add(childRef)
  290. } else {
  291. this.childrenShown.delete(childRef)
  292. }
  293. }
  294. },
  295. updated () {
  296. // Monitor changes to content size, update styles only when content sizes have changed,
  297. // that should be the only time we need to move the popover box if we don't care about scroll
  298. // or resize
  299. const content = this.$refs.content
  300. if (!content) return
  301. if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
  302. this.updateStyles()
  303. this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
  304. }
  305. },
  306. mounted () {
  307. this.teleport = true
  308. let scrollable = this.$refs.trigger.closest('.column.-scrollable') ||
  309. this.$refs.trigger.closest('.mobile-notifications')
  310. if (!scrollable) scrollable = window
  311. this.scrollable = scrollable
  312. let parent = this.$parent
  313. while (parent && parent.$.type.name !== 'Popover') {
  314. parent = parent.$parent
  315. }
  316. this.parentPopover = parent
  317. },
  318. beforeUnmount () {
  319. this.hidePopover()
  320. }
  321. }
  322. export default Popover