This commit is contained in:
parent
7a15361e17
commit
c6ac4466f0
6 changed files with 360 additions and 812 deletions
|
@ -1,17 +1,4 @@
|
|||
{% macro topic_comment_action_template() -%}
|
||||
{% if request.user.is_authenticated -%}
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="{{ _('Comment vote button group') }}">
|
||||
<button class="btn btn-outline-secondary text-success vote-up" data-bs-toggle="tooltip" title="{{ _('Vote up') }}">
|
||||
<i class="fa fa-fw fa-thumbs-o-up" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ _('Vote up') }}</span>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary text-danger vote-down" data-bs-toggle="tooltip" title="{{ _('Vote down') }}">
|
||||
<i class="fa fa-fw fa-thumbs-o-down" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ _('Vote down') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif -%}
|
||||
|
||||
<a class="btn btn-outline-secondary btn-sm expand-comments-down" data-bs-toggle="tooltip" title="{{ _('Show all previous comments') }}" tabindex="0">
|
||||
<i class="icon" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ _('Show all previous comments') }}</span>
|
||||
|
|
|
@ -4,9 +4,12 @@ import {
|
|||
unsetButtonLoading
|
||||
} from './utils/common.ts'
|
||||
import { Paginator } from './utils/paginator.ts'
|
||||
import { add as popoverAdd } from './utils/popover.ts'
|
||||
import {
|
||||
type ForumPopoverEl, add as popoverAdd, teardown as popoverTeardown
|
||||
} from './utils/popover.ts'
|
||||
import { initAllTimeElements } from './utils/time-actualizer.ts'
|
||||
import { add as usernameAdd } from './utils/username.ts'
|
||||
import { addOnremoveCallback } from './utils/mutation-observer.ts'
|
||||
|
||||
class LoadArchivedError extends Error {
|
||||
elSvg?: SVGElement
|
||||
|
@ -104,8 +107,8 @@ class TopicGroup {
|
|||
throw error
|
||||
}
|
||||
|
||||
private getPopoverContent(element: HTMLAnchorElement): string {
|
||||
const slug = element.dataset.slug
|
||||
private getPopoverContent(el: ForumPopoverEl<HTMLAnchorElement>): string {
|
||||
const slug = el.dataset.slug
|
||||
if (slug == null) return '?'
|
||||
const tipTemplate = this.root.querySelector(
|
||||
`.forum-topic-tooltip-template[data-slug="${slug}"]`)
|
||||
|
@ -124,17 +127,17 @@ class TopicGroup {
|
|||
const topicLinkElements =
|
||||
this.root.querySelectorAll<HTMLAnchorElement>('.topic-link')
|
||||
for (const node of topicLinkElements) {
|
||||
popoverAdd<HTMLAnchorElement>(node, {
|
||||
callbacks: { getContent: this.getPopoverContent.bind(this) },
|
||||
html: true,
|
||||
sanitize: false,
|
||||
delay: {
|
||||
show: 250,
|
||||
hide: 500
|
||||
},
|
||||
customClass: 'topicgroup-popover-wrapper',
|
||||
showOnClick: false
|
||||
popoverAdd(node, {
|
||||
popoverOpts: {
|
||||
content: this.getPopoverContent.bind(this),
|
||||
html: true,
|
||||
sanitize: false,
|
||||
delay: { show: 250, hide: 500 },
|
||||
customClass: 'topicgroup-popover-wrapper',
|
||||
container: <HTMLDivElement>node.parentElement
|
||||
}
|
||||
})
|
||||
addOnremoveCallback(node, popoverTeardown)
|
||||
}
|
||||
this.wrappers.paginators =
|
||||
this.root.querySelectorAll(this.options.selectors.paginationWrapper)
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { getCookie, getSvgIcon, loaderTemplate } from './common'
|
||||
import {
|
||||
type ForumPopoverEl,
|
||||
type ForumPopoverTipEl,
|
||||
add as popoverAdd,
|
||||
type ForumPopoverEl, type ForumPopoverTipEl, add as popoverAdd,
|
||||
teardown as popoverTeardown
|
||||
} from './new-popover.ts'
|
||||
} from './popover.ts'
|
||||
import { urlTemplates, stringsPassed } from '../topic-comment-listing'
|
||||
import { mdiLinkOff, mdiThumbUp, mdiThumbDown } from '@mdi/js'
|
||||
import { add as usernameAdd } from './username.ts'
|
||||
|
|
|
@ -1,306 +0,0 @@
|
|||
import Popover from 'bootstrap/js/src/popover.js'
|
||||
import { addOnremoveCallback } from './mutation-observer'
|
||||
|
||||
export type ForumPopoverEl<ElCls extends HTMLElement> = ElCls & {
|
||||
forumPopoverConfig: StoredConfigType<ElCls>
|
||||
forumPopoverTip?: ForumPopoverTipEl<ElCls>
|
||||
}
|
||||
|
||||
export type ForumPopoverTipEl<ElCls extends HTMLElement> = ElCls & {
|
||||
forumPopoverEl: ForumPopoverEl<ElCls>
|
||||
}
|
||||
|
||||
type ComputedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
delay: { show: number, hide: number }
|
||||
trigger: 'manual'
|
||||
content: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
}
|
||||
|
||||
type PassedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
content?: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
}
|
||||
|
||||
interface CallbacksType<ElCls extends HTMLElement> {
|
||||
onTipInserted?: (el: ForumPopoverEl<ElCls>) => void
|
||||
onTipRemoved?: (el: ForumPopoverEl<ElCls>) => void
|
||||
}
|
||||
|
||||
interface InitConfigType<ElCls extends HTMLElement> {
|
||||
popoverOpts?: PassedPopoverOpts<ElCls>
|
||||
callbacks?: CallbacksType<ElCls>
|
||||
}
|
||||
|
||||
interface StoredConfigType<ElCls extends HTMLElement> {
|
||||
popoverObj: Popover
|
||||
computedOpts: ComputedPopoverOpts<ElCls>
|
||||
showTimeoutId?: ReturnType<typeof setTimeout>
|
||||
hideTimeoutId?: ReturnType<typeof setTimeout>
|
||||
isShown: boolean
|
||||
isPointerenteredElement: boolean
|
||||
isPointerEnteredTip: boolean
|
||||
isFocusedElement: boolean
|
||||
isFocusedTip: boolean
|
||||
callbacks?: CallbacksType<ElCls>
|
||||
origGetContent?: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
cachedContent?: string | Element
|
||||
}
|
||||
|
||||
function onFocusinTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
console.debug('onFocusinTip', document.activeElement)
|
||||
this.forumPopoverEl.forumPopoverConfig.isFocusedTip = true
|
||||
showPopover(this.forumPopoverEl.forumPopoverConfig)
|
||||
}
|
||||
|
||||
function onFocusoutTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
console.debug('onFocusoutTip', document.activeElement)
|
||||
this.forumPopoverEl.forumPopoverConfig.isFocusedTip = false
|
||||
hidePopover(this.forumPopoverEl.forumPopoverConfig)
|
||||
}
|
||||
|
||||
function onTipRemoved<ElCls extends HTMLElement>(
|
||||
elTip: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onTipRemoved', elTip)
|
||||
elTip.removeEventListener('pointerenter', onPointerEnterTip)
|
||||
elTip.removeEventListener('pointerleave', onPointerLeaveTip)
|
||||
elTip.removeEventListener('focusin', onFocusinTip)
|
||||
elTip.removeEventListener('focusout', onFocusoutTip)
|
||||
const config = elTip.forumPopoverEl.forumPopoverConfig
|
||||
if (config.callbacks?.onTipRemoved) {
|
||||
config.callbacks.onTipRemoved(elTip.forumPopoverEl)
|
||||
}
|
||||
config.isPointerenteredElement = false
|
||||
config.isFocusedTip = false
|
||||
delete elTip.forumPopoverEl.forumPopoverTip
|
||||
// @ts-expect-error Allowed because element will get destroyed
|
||||
delete elTip.forumPopoverEl
|
||||
}
|
||||
|
||||
function onTipInserted<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
const config = this.forumPopoverConfig
|
||||
// @ts-expect-error `tip` exists, it's just not in the type definition
|
||||
const elTip = <ForumPopoverTipEl<ElCls>>config.popoverObj.tip
|
||||
elTip.forumPopoverEl = this
|
||||
this.forumPopoverTip = elTip
|
||||
if (config.callbacks?.onTipInserted) config.callbacks.onTipInserted(this)
|
||||
elTip.addEventListener('pointerenter', onPointerEnterTip)
|
||||
elTip.addEventListener('pointerleave', onPointerLeaveTip)
|
||||
elTip.addEventListener('focusin', onFocusinTip)
|
||||
elTip.addEventListener('focusout', onFocusoutTip)
|
||||
addOnremoveCallback(elTip, onTipRemoved)
|
||||
}
|
||||
|
||||
function onPointerEnterTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerEnterTip', this, ev)
|
||||
const config = this.forumPopoverEl.forumPopoverConfig
|
||||
config.isPointerEnteredTip = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onPointerLeaveTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerLeaveTip', this)
|
||||
const config = this.forumPopoverEl.forumPopoverConfig
|
||||
config.isPointerEnteredTip = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onFocusinElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusinElement', document.activeElement)
|
||||
const config = this.forumPopoverConfig
|
||||
config.isFocusedElement = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onFocusoutElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusoutElement', document.activeElement)
|
||||
const config = this.forumPopoverConfig
|
||||
config.isFocusedElement = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onPointerEnterElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerenterElement')
|
||||
const config = this.forumPopoverConfig
|
||||
config.isPointerenteredElement = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onPointerLeaveElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerleaveElement')
|
||||
const config = this.forumPopoverConfig
|
||||
config.isPointerenteredElement = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onPopoverHidden<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
const config = this.forumPopoverConfig
|
||||
config.isShown = config.isFocusedTip = config.isPointerEnteredTip = false
|
||||
}
|
||||
|
||||
// The content() function is executed twice (probable bootstrap bug):
|
||||
// use this function as an umbrella to only call it once
|
||||
function onGetContent<ElCls extends HTMLElement>(
|
||||
el: ForumPopoverEl<ElCls>
|
||||
): string | Element {
|
||||
if (el.forumPopoverConfig.cachedContent == null) {
|
||||
el.forumPopoverConfig.cachedContent = el.forumPopoverConfig.origGetContent
|
||||
? el.forumPopoverConfig.origGetContent(el)
|
||||
: ''
|
||||
}
|
||||
return el.forumPopoverConfig.cachedContent
|
||||
}
|
||||
|
||||
function isForumPopoverEl<ElCls extends HTMLElement>(
|
||||
el: ElCls | ForumPopoverEl<ElCls>
|
||||
): el is ForumPopoverEl<ElCls> {
|
||||
return Object.hasOwn(el, 'forumPopoverConfig')
|
||||
}
|
||||
|
||||
function clearHideTimeoutId<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
if (config.hideTimeoutId == null) return
|
||||
clearTimeout(config.hideTimeoutId)
|
||||
delete config.hideTimeoutId
|
||||
}
|
||||
|
||||
function clearShowTimeoutId<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
if (config.showTimeoutId == null) return
|
||||
clearTimeout(config.showTimeoutId)
|
||||
delete config.showTimeoutId
|
||||
}
|
||||
|
||||
function showPopoverImmediately<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
delete config.showTimeoutId
|
||||
if (config.isShown) return
|
||||
if (
|
||||
!config.isFocusedElement &&
|
||||
!config.isPointerenteredElement
|
||||
) return
|
||||
config.isShown = true
|
||||
config.popoverObj.show()
|
||||
delete config.cachedContent
|
||||
}
|
||||
|
||||
function showPopover<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
// console.debug('showPopover', config)
|
||||
clearHideTimeoutId(config)
|
||||
if (config.showTimeoutId != null) return
|
||||
config.showTimeoutId =
|
||||
setTimeout(showPopoverImmediately, config.computedOpts.delay.show, config)
|
||||
}
|
||||
|
||||
function hidePopoverImmediately<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
delete config.hideTimeoutId
|
||||
if (
|
||||
!config.isShown ||
|
||||
config.isPointerenteredElement ||
|
||||
config.isPointerEnteredTip ||
|
||||
config.isFocusedElement ||
|
||||
config.isFocusedTip
|
||||
) return
|
||||
config.popoverObj.hide()
|
||||
}
|
||||
|
||||
function hidePopover<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
// console.debug('hidePopover', config)
|
||||
clearShowTimeoutId(config)
|
||||
if (config.hideTimeoutId != null) return
|
||||
config.hideTimeoutId =
|
||||
setTimeout(hidePopoverImmediately, config.computedOpts.delay.hide, config)
|
||||
}
|
||||
|
||||
function computeOpts<ElCls extends HTMLElement>(
|
||||
origOpts?: PassedPopoverOpts<ElCls>
|
||||
): ComputedPopoverOpts<ElCls> {
|
||||
const computedOpts: ComputedPopoverOpts<ElCls> = {
|
||||
...origOpts,
|
||||
delay: { show: 250, hide: 0 },
|
||||
trigger: 'manual',
|
||||
content: onGetContent
|
||||
}
|
||||
if (origOpts?.delay != null) {
|
||||
computedOpts.delay = {
|
||||
show: typeof origOpts.delay === 'number'
|
||||
? origOpts.delay
|
||||
: origOpts.delay.show,
|
||||
hide: typeof origOpts.delay === 'number'
|
||||
? origOpts.delay
|
||||
: origOpts.delay.hide
|
||||
}
|
||||
}
|
||||
return computedOpts
|
||||
}
|
||||
|
||||
export function add<ElCls extends HTMLElement>(
|
||||
el: ElCls, config: InitConfigType<ElCls>
|
||||
): void {
|
||||
if (isForumPopoverEl(el)) return
|
||||
const computedOpts = computeOpts<ElCls>(config.popoverOpts)
|
||||
const popoverConfig: StoredConfigType<ElCls> = {
|
||||
popoverObj: new Popover(el, <Partial<Popover.Options>>computedOpts),
|
||||
computedOpts,
|
||||
isShown: false,
|
||||
isPointerenteredElement: false,
|
||||
isPointerEnteredTip: false,
|
||||
isFocusedElement: false,
|
||||
isFocusedTip: false,
|
||||
callbacks: config.callbacks,
|
||||
origGetContent: config.popoverOpts?.content
|
||||
}
|
||||
Object.defineProperty(el, 'forumPopoverConfig', {
|
||||
value: popoverConfig,
|
||||
configurable: true
|
||||
})
|
||||
el.addEventListener('inserted.bs.popover', onTipInserted)
|
||||
el.addEventListener('hidden.bs.popover', onPopoverHidden)
|
||||
el.addEventListener('focusin', onFocusinElement)
|
||||
el.addEventListener('focusout', onFocusoutElement)
|
||||
el.addEventListener('pointerenter', onPointerEnterElement)
|
||||
el.addEventListener('pointerleave', onPointerLeaveElement)
|
||||
}
|
||||
|
||||
export function teardown<ElCls extends HTMLElement>(el: ElCls): void {
|
||||
if (!isForumPopoverEl(el)) return
|
||||
el.forumPopoverConfig.popoverObj.dispose()
|
||||
el.removeEventListener('inserted.bs.popover', onTipInserted)
|
||||
el.removeEventListener('hidden.bs.popover', onPopoverHidden)
|
||||
el.removeEventListener('focusin', onFocusinElement)
|
||||
el.removeEventListener('focusout', onFocusoutElement)
|
||||
el.removeEventListener('pointerenter', onPointerEnterElement)
|
||||
el.removeEventListener('pointerleave', onPointerLeaveElement)
|
||||
// @ts-expect-error Popover is being destroyed so it's okay
|
||||
delete el.forumPopoverConfig
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import { getScrollTop, getSvgIcon, extractTemplateContent } from './common.ts'
|
||||
import Tooltip from 'bootstrap/js/src/tooltip.js'
|
||||
import { add as popoverAdd } from './popover.ts'
|
||||
import {
|
||||
type ForumPopoverEl, add as popoverAdd, teardown as popoverTeardown
|
||||
} from './popover.ts'
|
||||
import { add as toastAdd } from './toast.ts'
|
||||
import { add as usernameAdd } from './username.ts'
|
||||
import { add as timeActualizerAdd } from './time-actualizer.ts'
|
||||
|
@ -84,29 +86,46 @@ export class OneComment {
|
|||
toastAdd({ message: stringsPassed.noClipboardError })
|
||||
}
|
||||
|
||||
private onPopoverDomInsertedCommentAction(el: HTMLButtonElement): void {
|
||||
}
|
||||
|
||||
private onPopoverDomRemovedCommentAction(el: HTMLButtonElement): void {
|
||||
}
|
||||
|
||||
private onTeardownActionsPopoverContent(el: HTMLButtonElement): void {
|
||||
private onPopoverTipInsertedCommentAction(
|
||||
el: ForumPopoverEl<HTMLButtonElement>
|
||||
): void {
|
||||
if (!this.actionsPopoverReferences) return
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsDown).dispose()
|
||||
this.actionsPopoverReferences.elExpandCommentsDown)
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUp) {
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUp).dispose()
|
||||
this.actionsPopoverReferences.elExpandCommentsUp)
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUpRecursive) {
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive).dispose()
|
||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive)
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsInThread) {
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread)
|
||||
}
|
||||
delete this.actionsPopoverReferences
|
||||
}
|
||||
|
||||
private onPopoverTipRemovedCommentAction(el: HTMLButtonElement): void {
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsDown)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUp) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUp)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUpRecursive) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsInThread) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread
|
||||
).dispose()
|
||||
)?.dispose()
|
||||
}
|
||||
delete this.actionsPopoverReferences
|
||||
}
|
||||
|
@ -115,8 +134,7 @@ export class OneComment {
|
|||
const commentLink = event.currentTarget
|
||||
if (!(commentLink instanceof HTMLAnchorElement)) return
|
||||
event.preventDefault()
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||
if (!navigator.clipboard) {
|
||||
if (navigator.clipboard == null) {
|
||||
this.onErrorClipboardCopy()
|
||||
return
|
||||
}
|
||||
|
@ -201,7 +219,9 @@ export class OneComment {
|
|||
}
|
||||
}
|
||||
|
||||
private initializePopoverOfCommentActions(): HTMLDivElement {
|
||||
private getContentCommentActions(
|
||||
el: ForumPopoverEl<HTMLButtonElement>
|
||||
): HTMLElement {
|
||||
const elRoot = <HTMLDivElement>elCommentActionTemplate.cloneNode(true)
|
||||
this.actionsPopoverReferences = {
|
||||
elContentRoot: elRoot
|
||||
|
@ -212,7 +232,6 @@ export class OneComment {
|
|||
if (this.elPrevCommentLink) {
|
||||
elAnchorEcd.getElementsByClassName('icon')[0]
|
||||
.replaceWith(getSvgIcon(mdiArrowLeft))
|
||||
Tooltip.getOrCreateInstance(elAnchorEcd)
|
||||
this.actionsPopoverReferences.elExpandCommentsDown = elAnchorEcd
|
||||
} else elAnchorEcd.remove()
|
||||
const elAnchorEcu =
|
||||
|
@ -224,10 +243,8 @@ export class OneComment {
|
|||
if (this.oneReplies.size) {
|
||||
elAnchorEcu.getElementsByClassName('icon')[0]
|
||||
.replaceWith(getSvgIcon(mdiArrowRight))
|
||||
Tooltip.getOrCreateInstance(elAnchorEcu)
|
||||
elAnchorEcur.getElementsByClassName('icon')[0]
|
||||
.replaceWith(getSvgIcon(myArrowRightDouble))
|
||||
Tooltip.getOrCreateInstance(elAnchorEcur)
|
||||
this.actionsPopoverReferences.elExpandCommentsUp = elAnchorEcu
|
||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive = elAnchorEcur
|
||||
} else {
|
||||
|
@ -240,7 +257,6 @@ export class OneComment {
|
|||
if (this.oneReplies.size || this.elPrevCommentLink) {
|
||||
elAnchorEcit.getElementsByClassName('icon')[0]
|
||||
.replaceWith(getSvgIcon(mdiArrowLeftRight))
|
||||
Tooltip.getOrCreateInstance(elAnchorEcit)
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread = elAnchorEcit
|
||||
} else elAnchorEcit.remove()
|
||||
this.initPopoverLinks()
|
||||
|
@ -254,30 +270,24 @@ export class OneComment {
|
|||
}
|
||||
this.elBtnCommentActions.replaceChildren(elSvgCog.cloneNode(true))
|
||||
popoverAdd(this.elBtnCommentActions, {
|
||||
popoverOpts: {
|
||||
content: el => {
|
||||
return this.getContentCommentActions(el)
|
||||
},
|
||||
html: true,
|
||||
delay: { show: 500, hide: 500 },
|
||||
sanitize: false,
|
||||
customClass: 'forum-topic-comment-action-popovercontent',
|
||||
container: <HTMLDivElement>this.elBtnCommentActions.parentElement
|
||||
},
|
||||
callbacks: {
|
||||
getContent: (el) => {
|
||||
return this.initializePopoverOfCommentActions()
|
||||
onTipInserted: el => {
|
||||
this.onPopoverTipInsertedCommentAction(el)
|
||||
},
|
||||
onDomInsertedTip: (el: HTMLButtonElement) => {
|
||||
this.onPopoverDomInsertedCommentAction(el)
|
||||
},
|
||||
onDomRemovedTip: (el) => {
|
||||
this.onPopoverDomRemovedCommentAction(el)
|
||||
},
|
||||
onInvalidateContent: (el: HTMLButtonElement) => {
|
||||
this.onTeardownActionsPopoverContent(el)
|
||||
},
|
||||
onTeardown: (el: HTMLButtonElement) => {
|
||||
this.onTeardownActionsPopoverContent(el)
|
||||
onTipRemoved: el => {
|
||||
this.onPopoverTipRemovedCommentAction(el)
|
||||
}
|
||||
},
|
||||
html: true,
|
||||
delay: {
|
||||
show: 500, hide: 500
|
||||
},
|
||||
sanitize: false,
|
||||
customClass: 'forum-topic-comment-action-popovercontent',
|
||||
showOnClick: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -327,6 +337,9 @@ export class OneComment {
|
|||
for (const el of this.oneReplies.values()) this.teardownOneReply(el)
|
||||
this.oneReplies.clear()
|
||||
this.teardownVotingValue()
|
||||
if (this.elBtnCommentActions.parentElement) {
|
||||
popoverTeardown(this.elBtnCommentActions)
|
||||
}
|
||||
}
|
||||
|
||||
flashHighlight(): void {
|
||||
|
|
|
@ -1,453 +1,306 @@
|
|||
import Popover from 'bootstrap/js/src/popover.js'
|
||||
import { addOnremoveCallback } from './mutation-observer.ts'
|
||||
import { addOnremoveCallback } from './mutation-observer'
|
||||
|
||||
interface PopoverInitOptions<WatchedType> {
|
||||
callbacks: {
|
||||
getTitle?: (element: WatchedType) => string | HTMLElement
|
||||
getContent: (element: WatchedType) => string | HTMLElement
|
||||
onDomInsertedTip?: (element: WatchedType) => void
|
||||
onDomRemovedTip?: (element: WatchedType) => void
|
||||
onInvalidateContent?: (element: WatchedType) => void
|
||||
onInvalidateTitle?: (element: WatchedType) => void
|
||||
onTeardown?: (element: WatchedType) => void
|
||||
export type ForumPopoverEl<ElCls extends HTMLElement> = ElCls & {
|
||||
forumPopoverConfig: StoredConfigType<ElCls>
|
||||
forumPopoverTip?: ForumPopoverTipEl<ElCls>
|
||||
}
|
||||
|
||||
export type ForumPopoverTipEl<ElCls extends HTMLElement> = ElCls & {
|
||||
forumPopoverEl: ForumPopoverEl<ElCls>
|
||||
}
|
||||
|
||||
type ComputedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
delay: { show: number, hide: number }
|
||||
trigger: 'manual'
|
||||
content: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
}
|
||||
html: Popover.Options['html']
|
||||
delay: {
|
||||
show: number
|
||||
hide: number
|
||||
|
||||
type PassedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
content?: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
}
|
||||
showOnClick: boolean
|
||||
sanitize: Popover.Options['sanitize']
|
||||
customClass?: Popover.Options['customClass']
|
||||
initialContent?: string
|
||||
|
||||
interface CallbacksType<ElCls extends HTMLElement> {
|
||||
onTipInserted?: (el: ForumPopoverEl<ElCls>) => void
|
||||
onTipRemoved?: (el: ForumPopoverEl<ElCls>) => void
|
||||
}
|
||||
|
||||
export interface GetFunctionsReturnType {
|
||||
invalidateTitle: () => void
|
||||
invalidateContent: () => void
|
||||
isShown: () => boolean
|
||||
interface InitConfigType<ElCls extends HTMLElement> {
|
||||
popoverOpts?: PassedPopoverOpts<ElCls>
|
||||
callbacks?: CallbacksType<ElCls>
|
||||
}
|
||||
|
||||
interface BoundFunctions {
|
||||
mouseEnterPopover: (event: Event) => void
|
||||
mouseLeavePopover: (event: Event) => void
|
||||
clickElement: (event: Event) => void
|
||||
mouseEnterElement: (event: Event) => void
|
||||
mouseLeaveElement: (event: Event) => void
|
||||
focusInPopover: (event: Event) => void
|
||||
focusOutPopover: (event: Event) => void
|
||||
focusInElement: (event: Event) => void
|
||||
focusOutElement: (event: Event) => void
|
||||
popoverInserted: (event: Event) => void
|
||||
}
|
||||
|
||||
interface ObjConfig<WatchedType> {
|
||||
popoverObj?: Popover
|
||||
cachedTitle?: string | HTMLElement
|
||||
cachedContent?: string | HTMLElement
|
||||
isShown: boolean
|
||||
isTipHovered: boolean
|
||||
isPopoverFocused: boolean
|
||||
isElementHovered: boolean
|
||||
// isFocused is for keyboard focus
|
||||
isElementFocused: boolean
|
||||
interface StoredConfigType<ElCls extends HTMLElement> {
|
||||
popoverObj: Popover
|
||||
computedOpts: ComputedPopoverOpts<ElCls>
|
||||
showTimeoutId?: ReturnType<typeof setTimeout>
|
||||
hideTimeoutId?: ReturnType<typeof setTimeout>
|
||||
delay: {
|
||||
show: number
|
||||
hide: number
|
||||
}
|
||||
elPopover?: HTMLDivElement
|
||||
boundFunctions: BoundFunctions
|
||||
exportedFunctions: GetFunctionsReturnType
|
||||
callbacks: {
|
||||
getTitle?: (element: WatchedType) => string | HTMLElement
|
||||
getContent: (element: WatchedType) => string | HTMLElement
|
||||
onDomInsertedTip?: (el: WatchedType) => void
|
||||
onDomRemovedTip?: (el: WatchedType) => void
|
||||
onInvalidateTitle?: (el: WatchedType) => void
|
||||
onInvalidateContent?: (el: WatchedType) => void
|
||||
onTeardown?: (el: WatchedType) => void
|
||||
}
|
||||
showOnClick: boolean
|
||||
isShown: boolean
|
||||
isPointerenteredElement: boolean
|
||||
isPointerEnteredTip: boolean
|
||||
isFocusedElement: boolean
|
||||
isFocusedTip: boolean
|
||||
callbacks?: CallbacksType<ElCls>
|
||||
origGetContent?: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
cachedContent?: string | Element
|
||||
}
|
||||
|
||||
class PopoverHandler {
|
||||
private readonly instances =
|
||||
new Map<HTMLElement, ObjConfig<HTMLElement>>()
|
||||
|
||||
private onPopoverInserted<WatchedType extends HTMLElement>(
|
||||
event: Event
|
||||
): void {
|
||||
const el = <WatchedType>event.currentTarget
|
||||
const config = this.getConfigForElement(el)
|
||||
config.elPopover = <HTMLDivElement>config.popoverObj?.tip
|
||||
const parentNode = el.parentNode
|
||||
if (!parentNode) return
|
||||
config.elPopover.addEventListener(
|
||||
'mouseenter', config.boundFunctions.mouseEnterPopover)
|
||||
config.elPopover.addEventListener(
|
||||
'mouseleave', config.boundFunctions.mouseLeavePopover)
|
||||
config.elPopover.addEventListener(
|
||||
'focusin', config.boundFunctions.focusInPopover)
|
||||
config.elPopover.addEventListener(
|
||||
'focusout', config.boundFunctions.focusOutPopover)
|
||||
const removeCallback = (elPopover: HTMLDivElement): void => {
|
||||
this.onPopoverRemoved(el, elPopover)
|
||||
}
|
||||
addOnremoveCallback(config.elPopover, removeCallback)
|
||||
if (config.callbacks.onDomInsertedTip) {
|
||||
config.callbacks.onDomInsertedTip(el)
|
||||
}
|
||||
}
|
||||
|
||||
private onPopoverRemoved<WatchedType extends HTMLElement>(
|
||||
el: WatchedType, elPopover: HTMLDivElement
|
||||
): void {
|
||||
const config = this.getConfigForElement(el)
|
||||
elPopover.removeEventListener(
|
||||
'mouseenter', config.boundFunctions.mouseEnterPopover)
|
||||
elPopover.removeEventListener(
|
||||
'mouseleave', config.boundFunctions.mouseLeavePopover)
|
||||
elPopover.removeEventListener(
|
||||
'focusin', config.boundFunctions.focusInPopover)
|
||||
elPopover.removeEventListener(
|
||||
'focusout', config.boundFunctions.focusOutPopover)
|
||||
delete config.elPopover
|
||||
if (config.callbacks.onDomRemovedTip) config.callbacks.onDomRemovedTip(el)
|
||||
}
|
||||
|
||||
private onMouseenterPopover<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
config.isTipHovered = true
|
||||
}
|
||||
|
||||
private onMouseleavePopover<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
config.isTipHovered = false
|
||||
this.hidePopover(config)
|
||||
}
|
||||
|
||||
private onClickElement(event: Event): void {
|
||||
const config = this.getConfigForEvent(event)
|
||||
config.isElementHovered = true
|
||||
this.showPopoverImmediately(config)
|
||||
}
|
||||
|
||||
private onMouseenterElement(event: Event): void {
|
||||
const config = this.getConfigForEvent(event)
|
||||
config.isElementHovered = true
|
||||
this.showPopover(config)
|
||||
}
|
||||
|
||||
private onMouseleaveElement(event: Event): void {
|
||||
const config = this.getConfigForEvent(event)
|
||||
config.isElementHovered = false
|
||||
this.hidePopover(config)
|
||||
}
|
||||
|
||||
private onFocusinPopover<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
config.isPopoverFocused = true
|
||||
this.showPopover(config)
|
||||
}
|
||||
|
||||
private onFocusoutPopover<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
config.isPopoverFocused = false
|
||||
this.hidePopover(config)
|
||||
}
|
||||
|
||||
private onFocusinElement(event: Event): void {
|
||||
const config = this.getConfigForEvent(event)
|
||||
config.isElementFocused = true
|
||||
this.showPopover(config)
|
||||
}
|
||||
|
||||
private onFocusoutElement(event: Event): void {
|
||||
const config = this.getConfigForEvent(event)
|
||||
config.isElementFocused = false
|
||||
this.hidePopover(config)
|
||||
}
|
||||
|
||||
private getConfigForElement<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): ObjConfig<WatchedType> {
|
||||
const config: ObjConfig<WatchedType> | undefined =
|
||||
this.instances.get(element)
|
||||
if (!config) {
|
||||
throw Error(`PopoverHandler: No instance for ${element.outerHTML}`)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
private getConfigForEvent<WatchedType extends HTMLElement>(
|
||||
event: Event
|
||||
): ObjConfig<WatchedType> {
|
||||
const element = <WatchedType>event.currentTarget
|
||||
return this.getConfigForElement(element)
|
||||
}
|
||||
|
||||
private clearHideTimeoutId<WatchedType>(
|
||||
config: ObjConfig<WatchedType>
|
||||
): void {
|
||||
if (config.hideTimeoutId == null) return
|
||||
clearTimeout(config.hideTimeoutId)
|
||||
delete config.hideTimeoutId
|
||||
}
|
||||
|
||||
private clearShowTimeoutId<WatchedType>(
|
||||
config: ObjConfig<WatchedType>
|
||||
): void {
|
||||
if (config.showTimeoutId == null) return
|
||||
clearTimeout(config.showTimeoutId)
|
||||
delete config.showTimeoutId
|
||||
}
|
||||
|
||||
private showPopoverImmediately<WatchedType>(
|
||||
config: ObjConfig<WatchedType>
|
||||
): void {
|
||||
delete config.showTimeoutId
|
||||
if (config.isShown) return
|
||||
if (
|
||||
!config.isElementFocused &&
|
||||
!config.isElementHovered
|
||||
) return
|
||||
config.popoverObj?.show()
|
||||
config.isShown = true
|
||||
}
|
||||
|
||||
private showPopover<WatchedType>(config: ObjConfig<WatchedType>): void {
|
||||
this.clearHideTimeoutId(config)
|
||||
if (config.showTimeoutId != null) return
|
||||
config.showTimeoutId = setTimeout(() => {
|
||||
this.showPopoverImmediately(config)
|
||||
}, config.delay.show)
|
||||
}
|
||||
|
||||
private hidePopover<WatchedType>(config: ObjConfig<WatchedType>): void {
|
||||
this.clearShowTimeoutId(config)
|
||||
if (config.hideTimeoutId != null) return
|
||||
config.hideTimeoutId = setTimeout(() => {
|
||||
delete config.hideTimeoutId
|
||||
if (
|
||||
!config.isShown ||
|
||||
config.isTipHovered ||
|
||||
config.isElementFocused ||
|
||||
config.isElementHovered ||
|
||||
config.isPopoverFocused
|
||||
) return
|
||||
config.popoverObj?.hide()
|
||||
delete config.elPopover
|
||||
config.isShown = false
|
||||
}, config.delay.hide)
|
||||
}
|
||||
|
||||
private invalidateTitle<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
delete config.cachedTitle
|
||||
if (config.callbacks.onInvalidateTitle) {
|
||||
config.callbacks.onInvalidateTitle(element)
|
||||
}
|
||||
if (!config.isShown || !config.callbacks.getTitle) return
|
||||
config.popoverObj?.setContent({
|
||||
'.popover-header': config.callbacks.getTitle(element)
|
||||
})
|
||||
}
|
||||
|
||||
private invalidateContent<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
delete config.cachedContent
|
||||
console.debug('invalidateContent', config.isShown)
|
||||
if (config.callbacks.onInvalidateContent) {
|
||||
config.callbacks.onInvalidateContent(element)
|
||||
}
|
||||
if (!config.isShown) return
|
||||
config.popoverObj?.setContent({
|
||||
'.popover-body': config.callbacks.getContent(element)
|
||||
})
|
||||
}
|
||||
|
||||
private teardown<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): void {
|
||||
const config = this.getConfigForElement(element)
|
||||
element.removeEventListener(
|
||||
'mouseenter', config.boundFunctions.mouseEnterElement)
|
||||
element.removeEventListener(
|
||||
'mouseleave', config.boundFunctions.mouseLeaveElement)
|
||||
if (config.showOnClick) {
|
||||
element.removeEventListener(
|
||||
'click', config.boundFunctions.clickElement)
|
||||
}
|
||||
element.removeEventListener(
|
||||
'focusin', config.boundFunctions.focusInElement)
|
||||
element.removeEventListener(
|
||||
'focusout', config.boundFunctions.focusOutElement)
|
||||
element.removeEventListener(
|
||||
'inserted.bs.popover', config.boundFunctions.popoverInserted)
|
||||
if (config.callbacks.onTeardown) config.callbacks.onTeardown(element)
|
||||
config.popoverObj?.dispose()
|
||||
delete config.popoverObj
|
||||
this.clearHideTimeoutId(config)
|
||||
this.clearShowTimeoutId(config)
|
||||
this.instances.delete(element)
|
||||
}
|
||||
|
||||
private initReturnedFunctions<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): GetFunctionsReturnType {
|
||||
return {
|
||||
invalidateTitle: (): void => {
|
||||
this.invalidateTitle(element)
|
||||
},
|
||||
invalidateContent: (): void => {
|
||||
this.invalidateContent(element)
|
||||
},
|
||||
isShown: (): boolean => {
|
||||
const config = this.getConfigForElement(element)
|
||||
return config.isShown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initBoundFunctions<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): BoundFunctions {
|
||||
return {
|
||||
mouseEnterPopover: (): void => { this.onMouseenterPopover(element) },
|
||||
mouseLeavePopover: (): void => { this.onMouseleavePopover(element) },
|
||||
clickElement: this.onClickElement.bind(this),
|
||||
mouseEnterElement: this.onMouseenterElement.bind(this),
|
||||
mouseLeaveElement: this.onMouseleaveElement.bind(this),
|
||||
focusInPopover: () => { this.onFocusinPopover(element) },
|
||||
focusOutPopover: () => { this.onFocusoutPopover(element) },
|
||||
focusInElement: this.onFocusinElement.bind(this),
|
||||
focusOutElement: this.onFocusoutElement.bind(this),
|
||||
popoverInserted: this.onPopoverInserted.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
private getTitle<WatchedType extends HTMLElement>(
|
||||
element: WatchedType,
|
||||
cbGetTitle: (element: WatchedType) => string | HTMLElement
|
||||
): string | HTMLElement {
|
||||
const objConfig = this.getConfigForElement(element)
|
||||
if (objConfig.cachedTitle != null) return objConfig.cachedTitle
|
||||
const content = objConfig.cachedTitle = cbGetTitle(element)
|
||||
return content
|
||||
}
|
||||
|
||||
private getContent<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): string | HTMLElement {
|
||||
const objConfig = this.getConfigForElement(element)
|
||||
if (objConfig.cachedContent != null) return objConfig.cachedContent
|
||||
const content = objConfig.cachedContent =
|
||||
objConfig.callbacks.getContent(element)
|
||||
return content
|
||||
}
|
||||
|
||||
private initPopoverObj<WatchedType extends HTMLElement>(
|
||||
element: WatchedType, parentNode: Element,
|
||||
options: PopoverInitOptions<WatchedType>
|
||||
): Popover {
|
||||
const popoverOpts: Partial<Popover.Options> = {
|
||||
container: parentNode,
|
||||
content: () => {
|
||||
return this.getContent(element)
|
||||
},
|
||||
placement: 'auto',
|
||||
trigger: 'manual',
|
||||
html: options.html,
|
||||
sanitize: options.sanitize,
|
||||
customClass: options.customClass ?? '',
|
||||
delay: options.delay
|
||||
}
|
||||
if (options.callbacks.getTitle) {
|
||||
const cbGetTitle = options.callbacks.getTitle
|
||||
popoverOpts.title = () => {
|
||||
return this.getTitle(element, cbGetTitle)
|
||||
}
|
||||
}
|
||||
return new Popover(element, popoverOpts)
|
||||
}
|
||||
|
||||
getFunctions<WatchedType extends HTMLElement>(
|
||||
element: WatchedType
|
||||
): GetFunctionsReturnType {
|
||||
return this.getConfigForElement(element).exportedFunctions
|
||||
}
|
||||
|
||||
setup<WatchedType extends HTMLElement>(
|
||||
element: WatchedType,
|
||||
options: PopoverInitOptions<WatchedType>
|
||||
): void {
|
||||
const parentNode = element.parentNode
|
||||
if (!(parentNode instanceof HTMLElement)) {
|
||||
throw Error(
|
||||
`PopoverHandler.add: parentNode of ${element.outerHTML} must be ` +
|
||||
'HTMLElement')
|
||||
}
|
||||
const popoverObj = this.initPopoverObj(element, parentNode, options)
|
||||
const boundFunctions = this.initBoundFunctions(element)
|
||||
const objConfig: ObjConfig<WatchedType> = {
|
||||
popoverObj,
|
||||
cachedContent: options.initialContent,
|
||||
isShown: false,
|
||||
isTipHovered: false,
|
||||
isPopoverFocused: false,
|
||||
isElementHovered: false,
|
||||
isElementFocused: false,
|
||||
delay: options.delay,
|
||||
exportedFunctions: this.initReturnedFunctions(element),
|
||||
boundFunctions,
|
||||
callbacks: {
|
||||
getTitle: options.callbacks.getTitle,
|
||||
getContent: options.callbacks.getContent,
|
||||
onDomInsertedTip: options.callbacks.onDomInsertedTip,
|
||||
onDomRemovedTip: options.callbacks.onDomRemovedTip,
|
||||
onInvalidateTitle: options.callbacks.onInvalidateTitle,
|
||||
onInvalidateContent: options.callbacks.onInvalidateContent,
|
||||
onTeardown: options.callbacks.onTeardown
|
||||
},
|
||||
showOnClick: options.showOnClick
|
||||
}
|
||||
this.instances.set(element, <ObjConfig<HTMLElement>>objConfig)
|
||||
element.addEventListener('mouseenter', boundFunctions.mouseEnterElement)
|
||||
element.addEventListener('mouseleave', boundFunctions.mouseLeaveElement)
|
||||
if (options.showOnClick) {
|
||||
element.addEventListener('click', boundFunctions.clickElement)
|
||||
}
|
||||
element.addEventListener('focusin', boundFunctions.focusInElement)
|
||||
element.addEventListener('focusout', boundFunctions.focusOutElement)
|
||||
element.addEventListener(
|
||||
'inserted.bs.popover', boundFunctions.popoverInserted)
|
||||
addOnremoveCallback(element, this.teardown.bind(this))
|
||||
}
|
||||
function onFocusinTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusinTip', document.activeElement)
|
||||
this.forumPopoverEl.forumPopoverConfig.isFocusedTip = true
|
||||
showPopover(this.forumPopoverEl.forumPopoverConfig)
|
||||
}
|
||||
|
||||
const popoverHandler = new PopoverHandler()
|
||||
|
||||
export function add<WatchedType extends HTMLElement>(
|
||||
element: WatchedType,
|
||||
options: PopoverInitOptions<WatchedType>): void {
|
||||
popoverHandler.setup(element, options)
|
||||
function onFocusoutTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusoutTip', document.activeElement)
|
||||
this.forumPopoverEl.forumPopoverConfig.isFocusedTip = false
|
||||
hidePopover(this.forumPopoverEl.forumPopoverConfig)
|
||||
}
|
||||
|
||||
export function getFunctions<WatchedType extends HTMLElement>(
|
||||
element: WatchedType): GetFunctionsReturnType {
|
||||
return popoverHandler.getFunctions(element)
|
||||
function onTipRemoved<ElCls extends HTMLElement>(
|
||||
elTip: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onTipRemoved', elTip)
|
||||
elTip.removeEventListener('pointerenter', onPointerEnterTip)
|
||||
elTip.removeEventListener('pointerleave', onPointerLeaveTip)
|
||||
elTip.removeEventListener('focusin', onFocusinTip)
|
||||
elTip.removeEventListener('focusout', onFocusoutTip)
|
||||
const config = elTip.forumPopoverEl.forumPopoverConfig
|
||||
if (config.callbacks?.onTipRemoved) {
|
||||
config.callbacks.onTipRemoved(elTip.forumPopoverEl)
|
||||
}
|
||||
config.isPointerenteredElement = false
|
||||
config.isFocusedTip = false
|
||||
delete elTip.forumPopoverEl.forumPopoverTip
|
||||
// @ts-expect-error Allowed because element will get destroyed
|
||||
delete elTip.forumPopoverEl
|
||||
}
|
||||
|
||||
function onTipInserted<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
const config = this.forumPopoverConfig
|
||||
// @ts-expect-error `tip` exists, it's just not in the type definition
|
||||
const elTip = <ForumPopoverTipEl<ElCls>>config.popoverObj.tip
|
||||
elTip.forumPopoverEl = this
|
||||
this.forumPopoverTip = elTip
|
||||
if (config.callbacks?.onTipInserted) config.callbacks.onTipInserted(this)
|
||||
elTip.addEventListener('pointerenter', onPointerEnterTip)
|
||||
elTip.addEventListener('pointerleave', onPointerLeaveTip)
|
||||
elTip.addEventListener('focusin', onFocusinTip)
|
||||
elTip.addEventListener('focusout', onFocusoutTip)
|
||||
addOnremoveCallback(elTip, onTipRemoved)
|
||||
}
|
||||
|
||||
function onPointerEnterTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerEnterTip', this, ev)
|
||||
const config = this.forumPopoverEl.forumPopoverConfig
|
||||
config.isPointerEnteredTip = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onPointerLeaveTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerLeaveTip', this)
|
||||
const config = this.forumPopoverEl.forumPopoverConfig
|
||||
config.isPointerEnteredTip = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onFocusinElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusinElement', document.activeElement)
|
||||
const config = this.forumPopoverConfig
|
||||
config.isFocusedElement = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onFocusoutElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onFocusoutElement', document.activeElement)
|
||||
const config = this.forumPopoverConfig
|
||||
config.isFocusedElement = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onPointerEnterElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerenterElement')
|
||||
const config = this.forumPopoverConfig
|
||||
config.isPointerenteredElement = true
|
||||
showPopover(config)
|
||||
}
|
||||
|
||||
function onPointerLeaveElement<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
// console.debug('onPointerleaveElement')
|
||||
const config = this.forumPopoverConfig
|
||||
config.isPointerenteredElement = false
|
||||
hidePopover(config)
|
||||
}
|
||||
|
||||
function onPopoverHidden<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverEl<ElCls>
|
||||
): void {
|
||||
const config = this.forumPopoverConfig
|
||||
config.isShown = config.isFocusedTip = config.isPointerEnteredTip = false
|
||||
}
|
||||
|
||||
// The content() function is executed twice (probable bootstrap bug):
|
||||
// use this function as an umbrella to only call it once
|
||||
function onGetContent<ElCls extends HTMLElement>(
|
||||
el: ForumPopoverEl<ElCls>
|
||||
): string | Element {
|
||||
if (el.forumPopoverConfig.cachedContent == null) {
|
||||
el.forumPopoverConfig.cachedContent = el.forumPopoverConfig.origGetContent
|
||||
? el.forumPopoverConfig.origGetContent(el)
|
||||
: ''
|
||||
}
|
||||
return el.forumPopoverConfig.cachedContent
|
||||
}
|
||||
|
||||
function isForumPopoverEl<ElCls extends HTMLElement>(
|
||||
el: ElCls | ForumPopoverEl<ElCls>
|
||||
): el is ForumPopoverEl<ElCls> {
|
||||
return Object.hasOwn(el, 'forumPopoverConfig')
|
||||
}
|
||||
|
||||
function clearHideTimeoutId<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
if (config.hideTimeoutId == null) return
|
||||
clearTimeout(config.hideTimeoutId)
|
||||
delete config.hideTimeoutId
|
||||
}
|
||||
|
||||
function clearShowTimeoutId<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
if (config.showTimeoutId == null) return
|
||||
clearTimeout(config.showTimeoutId)
|
||||
delete config.showTimeoutId
|
||||
}
|
||||
|
||||
function showPopoverImmediately<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
delete config.showTimeoutId
|
||||
if (config.isShown) return
|
||||
if (
|
||||
!config.isFocusedElement &&
|
||||
!config.isPointerenteredElement
|
||||
) return
|
||||
config.isShown = true
|
||||
config.popoverObj.show()
|
||||
delete config.cachedContent
|
||||
}
|
||||
|
||||
function showPopover<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
// console.debug('showPopover', config)
|
||||
clearHideTimeoutId(config)
|
||||
if (config.showTimeoutId != null) return
|
||||
config.showTimeoutId =
|
||||
setTimeout(showPopoverImmediately, config.computedOpts.delay.show, config)
|
||||
}
|
||||
|
||||
function hidePopoverImmediately<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
delete config.hideTimeoutId
|
||||
if (
|
||||
!config.isShown ||
|
||||
config.isPointerenteredElement ||
|
||||
config.isPointerEnteredTip ||
|
||||
config.isFocusedElement ||
|
||||
config.isFocusedTip
|
||||
) return
|
||||
config.popoverObj.hide()
|
||||
}
|
||||
|
||||
function hidePopover<ElCls extends HTMLElement>(
|
||||
config: StoredConfigType<ElCls>
|
||||
): void {
|
||||
// console.debug('hidePopover', config)
|
||||
clearShowTimeoutId(config)
|
||||
if (config.hideTimeoutId != null) return
|
||||
config.hideTimeoutId =
|
||||
setTimeout(hidePopoverImmediately, config.computedOpts.delay.hide, config)
|
||||
}
|
||||
|
||||
function computeOpts<ElCls extends HTMLElement>(
|
||||
origOpts?: PassedPopoverOpts<ElCls>
|
||||
): ComputedPopoverOpts<ElCls> {
|
||||
const computedOpts: ComputedPopoverOpts<ElCls> = {
|
||||
...origOpts,
|
||||
delay: { show: 250, hide: 0 },
|
||||
trigger: 'manual',
|
||||
content: onGetContent
|
||||
}
|
||||
if (origOpts?.delay != null) {
|
||||
computedOpts.delay = {
|
||||
show: typeof origOpts.delay === 'number'
|
||||
? origOpts.delay
|
||||
: origOpts.delay.show,
|
||||
hide: typeof origOpts.delay === 'number'
|
||||
? origOpts.delay
|
||||
: origOpts.delay.hide
|
||||
}
|
||||
}
|
||||
return computedOpts
|
||||
}
|
||||
|
||||
export function add<ElCls extends HTMLElement>(
|
||||
el: ElCls, config: InitConfigType<ElCls>
|
||||
): void {
|
||||
if (isForumPopoverEl(el)) return
|
||||
const computedOpts = computeOpts<ElCls>(config.popoverOpts)
|
||||
const popoverConfig: StoredConfigType<ElCls> = {
|
||||
popoverObj: new Popover(el, <Partial<Popover.Options>>computedOpts),
|
||||
computedOpts,
|
||||
isShown: false,
|
||||
isPointerenteredElement: false,
|
||||
isPointerEnteredTip: false,
|
||||
isFocusedElement: false,
|
||||
isFocusedTip: false,
|
||||
callbacks: config.callbacks,
|
||||
origGetContent: config.popoverOpts?.content
|
||||
}
|
||||
Object.defineProperty(el, 'forumPopoverConfig', {
|
||||
value: popoverConfig,
|
||||
configurable: true
|
||||
})
|
||||
el.addEventListener('inserted.bs.popover', onTipInserted)
|
||||
el.addEventListener('hidden.bs.popover', onPopoverHidden)
|
||||
el.addEventListener('focusin', onFocusinElement)
|
||||
el.addEventListener('focusout', onFocusoutElement)
|
||||
el.addEventListener('pointerenter', onPointerEnterElement)
|
||||
el.addEventListener('pointerleave', onPointerLeaveElement)
|
||||
}
|
||||
|
||||
export function teardown<ElCls extends HTMLElement>(el: ElCls): void {
|
||||
if (!isForumPopoverEl(el)) return
|
||||
el.forumPopoverConfig.popoverObj.dispose()
|
||||
el.removeEventListener('inserted.bs.popover', onTipInserted)
|
||||
el.removeEventListener('hidden.bs.popover', onPopoverHidden)
|
||||
el.removeEventListener('focusin', onFocusinElement)
|
||||
el.removeEventListener('focusout', onFocusoutElement)
|
||||
el.removeEventListener('pointerenter', onPointerEnterElement)
|
||||
el.removeEventListener('pointerleave', onPointerLeaveElement)
|
||||
// @ts-expect-error Popover is being destroyed so it's okay
|
||||
delete el.forumPopoverConfig
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue