More removal of bound functions

This commit is contained in:
László Károlyi 2023-12-22 19:48:04 +01:00
parent 95cd6d378e
commit e9df2ae400
Signed by: karolyi
GPG key ID: 2DCAF25E55735BFE
7 changed files with 499 additions and 445 deletions

View file

@ -39,14 +39,14 @@
</div>
{% endif -%}
</section>
<section class="pagination-comments">
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES) }}
<section class="pagination-comments-wrapper">
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES, extra_classnames='pagination-comments') }}
</section>
{% for comment in page_comments %}
{{ topic_comment_template(comment=comment, topic=topic, is_template=False) }}
{% endfor %}
<section class="pagination-comments">
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES) }}
<section class="pagination-comments-wrapper">
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES, extra_classnames='pagination-comments') }}
</section>
</article>

View file

@ -1,7 +1,7 @@
{% macro paginator_generic(page, adjacent_pages) -%}
{% macro paginator_generic(page, adjacent_pages, extra_classnames='') -%}
{% set page_number_list = paginator_generic_get_list(current_no=page.number, num_pages=page.paginator.num_pages, adjacent_pages=adjacent_pages) %}
{% if page_number_list %}
<ul class="pagination">
<ul class="pagination{% if extra_classnames %} {{ extra_classnames }}{% endif%}">
{%- for dict_page in page_number_list %}
{% if dict_page.type == 'number' -%}
<li class="page-item page-numbered

View file

@ -245,7 +245,7 @@
}
.pagination-comments {
.pagination-comments-wrapper {
.pagination {
justify-content: center;
}

View file

@ -2,8 +2,12 @@ import { type TemplateExecutor } from 'lodash'
import template from 'lodash/template.js'
import { addOnremoveCallback } from './utils/mutation-observer.ts'
import { getNavbarHeight, promiseWindowLoad } from './utils/common.ts'
import { Paginator } from './utils/paginator.ts'
import { OneComment, init as oneCommentInit } from './utils/one-comment.ts'
import { Paginator, type PaginatorRootElType } from './utils/paginator.ts'
import {
init as oneCommentInit, initElonecomment, type ElOnecommentType,
teardownOnecomment, isInViewport, deselectAsFocused, selectAsFocused,
jumpToSelf, flashHighlight, highlightOn, highlightOff
} from './utils/one-comment.ts'
export interface PassedStringsType {
commentUrlCopied: string
@ -72,7 +76,7 @@ interface TopicCommentListingOptionsType {
strings: PassedStringsType
}
export interface TopicCommentListingUrltemplateType {
export let urlTemplates: {
commentListing: TemplateExecutor
expandCommentsDown: TemplateExecutor
expandCommentsUp: TemplateExecutor
@ -83,14 +87,6 @@ export interface TopicCommentListingUrltemplateType {
getVotingValueDetails?: TemplateExecutor
}
export interface TopicBoundFunctionsType {
commentHighlightOn: (event: Event) => void
commentHighlightOff: (event: Event) => void
onClickJumpToComment: (event: Event) => void
getALink: (scrollToPk: number) => string
}
export let urlTemplates: TopicCommentListingUrltemplateType
export let stringsPassed: PassedStringsType
function initializeUrls(options: TopicCommentListingOptionsType): void {
@ -145,220 +141,221 @@ function initializeUrls(options: TopicCommentListingOptionsType): void {
}
}
type ElTopicCommentListingType = Document & {
forumTopicCommentListing: TopicCommentListing
}
function getElRoot(): ElTopicCommentListingType {
return <ElTopicCommentListingType>document
}
function onClickJumpToComment(event: MouseEvent): void {
const commentLink = event.currentTarget
if (!(commentLink instanceof HTMLAnchorElement)) return
const commentPk = parseInt(commentLink.dataset.forumLinkTo ?? '-1')
const obj = getElRoot().forumTopicCommentListing
const elOneComment = obj.comments.get(commentPk)
if (!elOneComment) return
event.preventDefault()
// This is an intentional click so add it to navigation history
history.pushState(null, '', getALink(obj, commentPk))
jumpToSelf(elOneComment).catch((e) => {
console.error(`jumpToSelf failed with ${e}`)
})
}
function getFirstOrLastCommentPk(obj: TopicCommentListing): number {
const keys = [...obj.comments.keys()]
if (keys.length === 1) return keys[0]
const firstPk = keys[0]
const lastPk = keys[keys.length - 1]
const elFirstComment: ElOnecommentType =
obj.comments.values().next().value
const firstPos = elFirstComment.getBoundingClientRect().top
return firstPos > 0 ? firstPk : lastPk
}
function onScrollFirst(): void {
const obj = getElRoot().forumTopicCommentListing
obj.isPageScrolled = true
}
function onScrollEnd(): void {
const obj = getElRoot().forumTopicCommentListing
const navbarHeight = getNavbarHeight()
const viewportHeight = document.documentElement.clientHeight
let selectedCommentPk = 0
for (const [thisCommentPk, elOneComment] of obj.comments.entries()) {
if (isInViewport(elOneComment, navbarHeight, viewportHeight)) {
selectedCommentPk = thisCommentPk
break
}
}
if (!selectedCommentPk) selectedCommentPk = getFirstOrLastCommentPk(obj)
if (obj.lastSelectedCommentPk === selectedCommentPk) return
const elOneCommentOld = obj.comments.get(obj.lastSelectedCommentPk)
if (elOneCommentOld) deselectAsFocused(elOneCommentOld.forumOneComment)
const elOneCommentNew = obj.comments.get(selectedCommentPk)
if (elOneCommentNew) selectAsFocused(elOneCommentNew.forumOneComment)
updateUrl(obj, selectedCommentPk)
obj.lastSelectedCommentPk = selectedCommentPk
}
async function onPaginate(
this: PaginatorRootElType, pageNo: number, elClicked: HTMLAnchorElement
): Promise<void> {
if (!urlTemplates.commentListingPageNo) {
console.error('urlTemplates.commentListingPageNo is unset')
return
}
const obj = getElRoot().forumTopicCommentListing
obj.paginators.forEach(paginator => {
paginator.setLoading(elClicked)
})
document.location.href = urlTemplates.commentListingPageNo({
topicSlug: obj.options.topicSlugOriginal,
pageId: pageNo
})
}
function onTeardownOnecomment(el: ElOnecommentType): void {
const obj = getElRoot().forumTopicCommentListing
obj.comments.delete(el.forumOneComment.commentPk)
teardownOnecomment(
el, onClickJumpToComment, commentHighlightOn, commentHighlightOff)
}
function getALink(obj: TopicCommentListing, scrollToPk: number): string {
switch (obj.options.listingMode) {
case 'commentListing':
return urlTemplates.commentListing({
topicSlug: obj.options.topicSlugOriginal, commentPk: scrollToPk
})
case 'expandCommentsDown':
return urlTemplates.expandCommentsDown({
topicSlug: obj.options.topicSlugOriginal,
commentPk: obj.options.commentPk,
scrollToPk
})
case 'expandCommentsUp':
return urlTemplates.expandCommentsUp({
topicSlug: obj.options.topicSlugOriginal,
commentPk: obj.options.commentPk,
scrollToPk
})
case 'expandCommentsUpRecursive':
return urlTemplates.expandCommentsUpRecursive({
topicSlug: obj.options.topicSlugOriginal,
commentPk: obj.options.commentPk,
scrollToPk
})
case 'expandCommentsEntireThread':
return urlTemplates.expandCommentsEntireThread({
topicSlug: obj.options.topicSlugOriginal,
commentPk: obj.options.commentPk,
scrollToPk
})
}
}
function commentHighlightOn(event: Event): void {
const target = event.target
if (!(target instanceof HTMLAnchorElement)) return
const commentPk = parseInt(target.dataset.forumLinkTo ?? '-1')
const obj = getElRoot().forumTopicCommentListing
const elOnecomment = obj.comments.get(commentPk)
if (!elOnecomment) return
highlightOn(elOnecomment)
}
function commentHighlightOff(event: Event): void {
const target = event.target
if (!(target instanceof HTMLAnchorElement)) return
const commentPk = parseInt(target.dataset.forumLinkTo ?? '-1')
const obj = getElRoot().forumTopicCommentListing
const elOnecomment = obj.comments.get(commentPk)
if (!elOnecomment) return
highlightOff(elOnecomment)
}
function updateUrl(obj: TopicCommentListing, commentPk: number): void {
const path = getALink(obj, commentPk)
if (!path || path === location.pathname) return
history.replaceState(null, '', path)
}
async function initialJumpToComment(obj: TopicCommentListing): Promise<void> {
let scrollToPk: number
if (obj.options.listingMode === 'commentListing') {
if (obj.options.commentPk == null) return
scrollToPk = obj.options.commentPk
} else {
if (obj.options.scrollToPk == null) return
scrollToPk = obj.options.scrollToPk
}
const elOnecomment = obj.comments.get(scrollToPk)
if (!elOnecomment) return
await promiseWindowLoad
// Don't jump to comment when the user scrolled the page before
// the onload event firing
if (obj.isPageScrolled) return
await jumpToSelf(elOnecomment)
await flashHighlight(elOnecomment)
}
class TopicCommentListing {
private readonly options: TopicCommentListingOptionsType
private boundFunctions!: TopicBoundFunctionsType
private root!: HTMLElement
private readonly comments = new Map<number, OneComment>()
private isPageScrolled = false
private lastSelectedCommentPk = 0
private readonly paginators = new Array<Paginator>()
readonly options: TopicCommentListingOptionsType
readonly comments = new Map<number, ElOnecommentType>()
isPageScrolled = false
lastSelectedCommentPk = 0
readonly paginators = new Array<Paginator>()
constructor (options: TopicCommentListingOptionsType) {
constructor (
root: ElTopicCommentListingType,
options: TopicCommentListingOptionsType
) {
this.options = options
this.initializeElOnecomments(root)
this.initializePaginators(root)
addEventListener('scrollend', onScrollEnd)
}
private commentHighlightOn(event: Event): void {
const target = event.target
if (!(target instanceof HTMLAnchorElement)) return
const commentPk = parseInt(target.dataset.forumLinkTo ?? '-1')
const oneComment = this.comments.get(commentPk)
if (!oneComment) return
oneComment.highlightOn()
}
private commentHighlightOff(event: Event): void {
const target = event.target
if (!(target instanceof HTMLAnchorElement)) return
const commentPk = parseInt(target.dataset.forumLinkTo ?? '-1')
const oneComment = this.comments.get(commentPk)
if (!oneComment) return
oneComment.highlightOff()
}
private onClickJumpToComment(event: Event): void {
const commentLink = event.currentTarget
if (!(commentLink instanceof HTMLAnchorElement)) return
const commentPk = parseInt(commentLink.dataset.forumLinkTo ?? '-1')
const oneComment = this.comments.get(commentPk)
if (!oneComment) return
event.preventDefault()
// This is an intentional click so add it to navigation history
history.pushState(null, '', this.getALink(commentPk))
oneComment.jumpToSelf()
}
private onScrollFirst(): void {
this.isPageScrolled = true
}
private onScrollEnd(): void {
const navbarHeight = getNavbarHeight()
const viewportHeight = document.documentElement.clientHeight
let selectedCommentPk: number = 0
for (const [thisCommentPk, oneComment] of this.comments.entries()) {
if (oneComment.isInViewport(navbarHeight, viewportHeight)) {
selectedCommentPk = thisCommentPk
break
}
}
if (!selectedCommentPk) selectedCommentPk = this.getFirstOrLastCommentPk()
if (this.lastSelectedCommentPk === selectedCommentPk) return
this.comments.get(this.lastSelectedCommentPk)?.deselectAsFocused()
this.updateUrl(selectedCommentPk)
this.comments.get(selectedCommentPk)?.selectAsFocused()
this.lastSelectedCommentPk = selectedCommentPk
}
private getFirstOrLastCommentPk(): number {
const keys = [...this.comments.keys()]
if (keys.length === 1) return keys[0]
const firstPk = keys[0]
const lastPk = keys[keys.length - 1]
const firstComment: OneComment = this.comments.values().next().value
const firstPos = firstComment.rootEl.getBoundingClientRect().top
return firstPos > 0 ? firstPk : lastPk
}
private getALink(scrollToPk: number): string {
switch (this.options.listingMode) {
case 'commentListing':
return urlTemplates.commentListing({
topicSlug: this.options.topicSlugOriginal, commentPk: scrollToPk
})
case 'expandCommentsDown':
return urlTemplates.expandCommentsDown({
topicSlug: this.options.topicSlugOriginal,
commentPk: this.options.commentPk,
scrollToPk
})
case 'expandCommentsUp':
return urlTemplates.expandCommentsUp({
topicSlug: this.options.topicSlugOriginal,
commentPk: this.options.commentPk,
scrollToPk
})
case 'expandCommentsUpRecursive':
return urlTemplates.expandCommentsUpRecursive({
topicSlug: this.options.topicSlugOriginal,
commentPk: this.options.commentPk,
scrollToPk
})
case 'expandCommentsEntireThread':
return urlTemplates.expandCommentsEntireThread({
topicSlug: this.options.topicSlugOriginal,
commentPk: this.options.commentPk,
scrollToPk
})
}
}
private updateUrl(commentPk: number): void {
const path = this.getALink(commentPk)
if (!path || path === location.pathname) return
history.replaceState(null, '', path)
}
private initializeOneComment(rootEl: HTMLElement): void {
const commentPk = parseInt(rootEl.dataset.forumCommentPk ?? '-1')
if (commentPk === -1) throw new Error('forumLinkTo not found')
this.comments.set(commentPk, new OneComment({
root: rootEl, commentPk, boundFunctions: this.boundFunctions
}))
addOnremoveCallback(rootEl, this.teardownOneComment.bind(this))
}
private teardownOneComment(rootEl: HTMLElement): void {
const strCommentId = rootEl.dataset.forumCommentPk
if (typeof strCommentId !== 'string') return
const commentPk = parseInt(strCommentId)
this.comments.get(commentPk)?.teardown()
this.comments.delete(commentPk)
}
private initializeBoundFunctions(): void {
addEventListener('scroll', this.onScrollFirst.bind(this), { once: true })
this.boundFunctions = {
commentHighlightOn: this.commentHighlightOn.bind(this),
commentHighlightOff: this.commentHighlightOff.bind(this),
onClickJumpToComment: this.onClickJumpToComment.bind(this),
getALink: this.getALink.bind(this)
}
}
private initializeWrappers(): void {
const root = document.getElementById(this.options.selectors.root)
if (!root) return
this.root = root
private initializeElOnecomments(root: ElTopicCommentListingType): void {
const commentWrappers = <HTMLCollectionOf<HTMLElement>>
this.root.getElementsByClassName(this.options.selectors.commentWrapper)
root.getElementsByClassName(this.options.selectors.commentWrapper)
for (const el of commentWrappers) {
this.initializeOneComment(el)
const commentPk = parseInt(el.dataset.forumCommentPk ?? '-1')
if (commentPk === -1) throw new Error('forumLinkTo not found')
const aLink = getALink(this, commentPk)
const elOneComment = initElonecomment(
el, commentPk, aLink, onClickJumpToComment, commentHighlightOn,
commentHighlightOff)
addOnremoveCallback(elOneComment, onTeardownOnecomment)
this.comments.set(commentPk, elOneComment)
}
}
private async onPaginate(pageNo: number): Promise<void> {
if (!urlTemplates.commentListingPageNo) {
console.error('urlTemplates.commentListingPageNo is unset')
return
}
document.location.href = urlTemplates.commentListingPageNo({
topicSlug: this.options.topicSlugOriginal,
pageId: pageNo
})
}
private initializePaginators(): void {
private initializePaginators(root: ElTopicCommentListingType): void {
if (this.options.listingMode !== 'commentListing') return
const wrappers =
<HTMLCollectionOf<HTMLElement>>
this.root.getElementsByClassName('pagination-comments')
<HTMLCollectionOf<HTMLUListElement>>
root.getElementsByClassName('pagination-comments')
for (const elRoot of wrappers) {
this.paginators.push(new Paginator({
root: elRoot,
callbacks: {
load: this.onPaginate.bind(this)
}
this.paginators.push(Paginator.add({
root: elRoot, callbacks: { load: onPaginate }
}))
}
}
private initialJumpToComment(): void {
let scrollToPk: number
if (this.options.listingMode === 'commentListing') {
if (this.options.commentPk == null) return
scrollToPk = this.options.commentPk
} else {
if (this.options.scrollToPk == null) return
scrollToPk = this.options.scrollToPk
}
const oneComment = this.comments.get(scrollToPk)
if (!oneComment) return
promiseWindowLoad.then(() => {
// Don't jump to comment when the user scrolled the page before
// the onload event firing
if (this.isPageScrolled) return
oneComment.jumpToSelf(() => {
oneComment.flashHighlight()
})
}, () => {
console.error('initial scroll resulted in failure')
})
}
initialize(): void {
this.initializeBoundFunctions()
this.initializeWrappers()
this.initializePaginators()
this.initialJumpToComment()
addEventListener('scrollend', this.onScrollEnd.bind(this))
}
}
export function init(options: TopicCommentListingOptionsType): void {
stringsPassed = options.strings
oneCommentInit()
initializeUrls(options)
stringsPassed = options.strings
const obj = new TopicCommentListing(options)
obj.initialize()
const root = getElRoot()
root.forumTopicCommentListing = new TopicCommentListing(root, options)
addEventListener('scroll', onScrollFirst, { once: true })
initialJumpToComment(root.forumTopicCommentListing).catch((e) => {
console.error('initial scroll resulted in failure:', e)
})
}

View file

@ -300,6 +300,10 @@ export function add(elRoot: HTMLAnchorElement, commentPk: number): void {
throw new Error(`VotingValue.add: ${elRoot.outerHTML} already initialized`)
}
storedObjs.set(elRoot, { commentPk })
const container = elRoot.parentElement
if (!container) {
throw new Error(`container of ${elRoot.outerHTML} is not a HTMLElement`)
}
popoverAdd(elRoot, {
callbacks: { onTipInserted, onTipRemoved },
popoverOpts: {
@ -311,7 +315,7 @@ export function add(elRoot: HTMLAnchorElement, commentPk: number): void {
hide: 250
},
title: stringsPassed.castedVotes,
container: <HTMLElement>elRoot.parentElement,
container,
// template: popoverTemplate,
customClass: 'voting-value-popover-wrapper'
}

View file

@ -9,9 +9,7 @@ import { add as timeActualizerAdd } from './time-actualizer.ts'
import {
add as votingValueAdd, teardown as votingValueTeardown
} from './comment-voting-details.ts'
import {
type TopicBoundFunctionsType, urlTemplates, stringsPassed
} from '../topic-comment-listing.ts'
import { urlTemplates, stringsPassed } from '../topic-comment-listing.ts'
import {
mdiArrowLeft, mdiArrowLeftRight, mdiArrowRight, mdiCogOutline
} from '@mdi/js'
@ -32,61 +30,171 @@ interface ActionsPopoverReferencesType {
elExpandCommentsInThread?: HTMLAnchorElement
}
interface OneCommentBoundFunctionsType extends TopicBoundFunctionsType {
onClickCommentNumber: (event: MouseEvent) => void
export type ElOnecommentType = HTMLElement & {
forumOneComment: OneComment
}
function getElRoot(el: HTMLElement): ElOnecommentType {
const elRoot = el.closest<ElOnecommentType>('section.topic-comment-wrapper')
if (!elRoot) {
throw new Error('Closest section.topic-comment-wrapper not found')
}
return elRoot
}
function getContentCommentActions(
el: ForumPopoverEl<HTMLButtonElement>
): HTMLElement {
const obj = getElRoot(el).forumOneComment
return obj.getContentCommentActions()
}
function onPopoverTipInsertedCommentAction(
el: ForumPopoverEl<HTMLButtonElement>
): void {
const obj = getElRoot(el).forumOneComment
obj.onPopoverTipInsertedCommentAction(el)
}
function onPopoverTipRemovedCommentAction(
el: ForumPopoverEl<HTMLButtonElement>
): void {
const obj = getElRoot(el).forumOneComment
obj.onPopoverTipRemovedCommentAction(el)
}
function onSuccessClipboardCopy(): void {
toastAdd({ message: stringsPassed.commentUrlCopied })
}
function onErrorClipboardCopy(): void {
toastAdd({ message: stringsPassed.noClipboardError })
}
function onClickCommentNumber(this: HTMLAnchorElement, ev: MouseEvent): void {
const commentLink = ev.currentTarget
if (!(commentLink instanceof HTMLAnchorElement)) return
ev.preventDefault()
if (navigator.clipboard == null) {
onErrorClipboardCopy()
return
}
navigator.clipboard.writeText(commentLink.href).then(
onSuccessClipboardCopy, onErrorClipboardCopy)
}
/**
* Will return `true` if at least 1/3 of the comment body is visible.
*/
export function isInViewport(
el: ElOnecommentType, zeroPoint: number, viewportHeight: number
): boolean {
const bodyInfo = el.forumOneComment.elBody.getBoundingClientRect()
const relBodyBottomY = bodyInfo.top + bodyInfo.height
// Comment is fully outside of the viewport
if (
relBodyBottomY <= zeroPoint || bodyInfo.top >= viewportHeight
) return false
// Top inside
if (bodyInfo.top >= zeroPoint) return true
// Top outside, bottom inside and above 60% height
if (relBodyBottomY >= viewportHeight * 0.6) return true
// Bottom inside but below 60% height
const relThirdBottomY = bodyInfo.top + bodyInfo.height / 3
// True if the top 1/3 of the comment body is inside
return relThirdBottomY > zeroPoint
}
export function selectAsFocused(obj: OneComment): void {
if (obj.elRoot.classList.contains('is-currently-focused')) return
obj.elRoot.classList.add('is-currently-focused')
}
export function deselectAsFocused(obj: OneComment): void {
if (!obj.elRoot.classList.contains('is-currently-focused')) return
obj.elRoot.classList.remove('is-currently-focused')
}
export async function jumpToSelf(el: ElOnecommentType): Promise<void> {
document.addEventListener('scrollend', () => {
if (document.activeElement instanceof HTMLAnchorElement) {
document.activeElement.blur()
}
}, { once: true })
const finishPromise = new Promise<void>((resolve) => {
document.addEventListener('scrollend', () => {
resolve()
}, { once: true })
})
scroll({ top: getScrollTop(el), behavior: 'auto' })
await finishPromise
}
export async function flashHighlight(el: ElOnecommentType): Promise<void> {
const finishPromise = new Promise<void>((resolve) => {
requestAnimationFrame(() => {
highlightOn(el)
requestAnimationFrame(() => {
highlightOff(el)
resolve()
})
})
})
await finishPromise
}
export function highlightOn(el: ElOnecommentType): void {
el.classList.add(highlightedClass)
}
export function highlightOff(el: ElOnecommentType): void {
el.classList.remove(highlightedClass)
}
export class OneComment {
readonly rootEl: HTMLElement
private readonly commentPk: number
private readonly boundFunctions: OneCommentBoundFunctionsType
private readonly oneReplies = new Map<number, HTMLDivElement>()
private elPrevCommentLink?: HTMLAnchorElement
private elAnchorCommentNumber!: HTMLAnchorElement
private readonly elBody: HTMLDivElement
private readonly elBtnCommentActions: HTMLButtonElement
readonly elRoot: HTMLElement
readonly commentPk: number
elAnchorCommentNumber!: HTMLAnchorElement
elPrevCommentLink?: HTMLAnchorElement
readonly oneReplies = new Map<number, HTMLDivElement>()
readonly elBtnCommentActions: HTMLButtonElement
readonly elBody: HTMLDivElement
private actionsPopoverReferences?: ActionsPopoverReferencesType
constructor ({ root, commentPk, boundFunctions }: {
root: HTMLElement
commentPk: number
boundFunctions: TopicBoundFunctionsType
}) {
this.rootEl = root
constructor (
elRoot: HTMLElement, commentPk: number, aLink: string,
onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
) {
this.elRoot = elRoot
this.commentPk = commentPk
this.boundFunctions = {
...boundFunctions,
onClickCommentNumber: this.onClickCommentNumber.bind(this)
}
const aEls = <HTMLCollectionOf<HTMLAnchorElement>>
root.getElementsByClassName('forum-username')
this.elBody = <HTMLDivElement>root.getElementsByClassName('card-block')[0]
this.elBody =
<HTMLDivElement>
elRoot.getElementsByClassName('card-block').item(0)
this.elBtnCommentActions =
<HTMLButtonElement>
root.getElementsByClassName('forum-comment-actions')[0]
elRoot.getElementsByClassName('forum-comment-actions').item(0)
const aEls = <HTMLCollectionOf<HTMLAnchorElement>>
elRoot.getElementsByClassName('forum-username')
for (const aEl of aEls) usernameAdd(aEl)
timeActualizerAdd(
<HTMLCollectionOf<HTMLTimeElement>>
root.getElementsByClassName('forum-time'))
elRoot.getElementsByClassName('forum-time'))
this.initVotingValue()
this.initializeCallbacks()
this.initializeCallbacks(
aLink, onClickJumpToComment, commentHighlightOn, commentHighlightOff)
}
private initVotingValue(): void {
const elRoot = this.rootEl.getElementsByClassName('voting-value')[0]
if (!(elRoot instanceof HTMLAnchorElement)) return
const elRoot = this.elRoot.getElementsByClassName('voting-value').item(0)
if (!(elRoot instanceof HTMLAnchorElement)) {
throw new Error('voting value on not found on comment')
}
votingValueAdd(elRoot, this.commentPk)
}
private onSuccessClipboardCopy(): void {
toastAdd({ message: stringsPassed.commentUrlCopied })
}
private onErrorClipboardCopy(): void {
toastAdd({ message: stringsPassed.noClipboardError })
}
private onPopoverTipInsertedCommentAction(
onPopoverTipInsertedCommentAction(
el: ForumPopoverEl<HTMLButtonElement>
): void {
if (!this.actionsPopoverReferences) return
@ -109,7 +217,9 @@ export class OneComment {
delete this.actionsPopoverReferences
}
private onPopoverTipRemovedCommentAction(el: HTMLButtonElement): void {
onPopoverTipRemovedCommentAction(
el: ForumPopoverEl<HTMLButtonElement>
): void {
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
Tooltip.getInstance(
this.actionsPopoverReferences.elExpandCommentsDown)?.dispose()
@ -130,69 +240,27 @@ export class OneComment {
delete this.actionsPopoverReferences
}
private onClickCommentNumber(event: MouseEvent): void {
const commentLink = event.currentTarget
if (!(commentLink instanceof HTMLAnchorElement)) return
event.preventDefault()
if (navigator.clipboard == null) {
this.onErrorClipboardCopy()
return
}
navigator.clipboard.writeText(commentLink.href).then(
this.onSuccessClipboardCopy.bind(this),
this.onErrorClipboardCopy.bind(this))
}
private initializeOneReply(el: HTMLDivElement): number {
const replyLink = <HTMLAnchorElement>
el.getElementsByClassName('forum-to-reply-comment')[0]
const commentPk = parseInt(replyLink.dataset.forumLinkTo ?? '-1')
if (commentPk === -1) throw new Error('forumLinkTo not found')
this.oneReplies.set(commentPk, el)
replyLink.addEventListener(
'mouseenter', this.boundFunctions.commentHighlightOn)
replyLink.addEventListener(
'mouseleave', this.boundFunctions.commentHighlightOff)
replyLink.addEventListener(
'focusin', this.boundFunctions.commentHighlightOn)
replyLink.addEventListener(
'focusout', this.boundFunctions.commentHighlightOff)
replyLink.addEventListener(
'click', this.boundFunctions.onClickJumpToComment)
private initializeOneReply(
elOneReply: HTMLDivElement, onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): number {
const el =
<HTMLAnchorElement>
elOneReply.getElementsByClassName('forum-to-reply-comment')[0]
const commentPk = parseInt(el.dataset.forumLinkTo ?? '-1')
if (commentPk === -1) throw new Error('forumLinkTo on reply not found')
this.oneReplies.set(commentPk, elOneReply)
el.addEventListener('mouseenter', commentHighlightOn)
el.addEventListener('mouseleave', commentHighlightOff)
el.addEventListener('focusin', commentHighlightOn)
el.addEventListener('focusout', commentHighlightOff)
el.addEventListener('click', onClickJumpToComment)
return commentPk
}
private teardownOneReply(el: HTMLDivElement): void {
const replyLink = <HTMLAnchorElement>
el.getElementsByClassName('forum-to-reply-comment')[0]
replyLink.removeEventListener(
'mouseenter', this.boundFunctions.commentHighlightOn)
replyLink.removeEventListener(
'mouseleave', this.boundFunctions.commentHighlightOff)
replyLink.removeEventListener(
'focusin', this.boundFunctions.commentHighlightOn)
replyLink.removeEventListener(
'focusout', this.boundFunctions.commentHighlightOff)
replyLink.removeEventListener(
'click', this.boundFunctions.onClickJumpToComment)
}
private initializePrevComment(prevCommentLink: HTMLAnchorElement): void {
this.elPrevCommentLink = prevCommentLink
this.elPrevCommentLink.addEventListener(
'mouseenter', this.boundFunctions.commentHighlightOn)
this.elPrevCommentLink.addEventListener(
'mouseleave', this.boundFunctions.commentHighlightOff)
this.elPrevCommentLink.addEventListener(
'focusin', this.boundFunctions.commentHighlightOn)
this.elPrevCommentLink.addEventListener(
'focusout', this.boundFunctions.commentHighlightOff)
this.elPrevCommentLink.addEventListener(
'click', this.boundFunctions.onClickJumpToComment)
}
private initPopoverLinks(): void {
const topicSlug = this.rootEl.dataset.forumTopicSlug
const topicSlug = this.elRoot.dataset.forumTopicSlug
if (typeof topicSlug !== 'string') return
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
this.actionsPopoverReferences.elExpandCommentsDown.href =
@ -220,32 +288,28 @@ export class OneComment {
}
}
private getContentCommentActions(
el: ForumPopoverEl<HTMLButtonElement>
): HTMLElement {
getContentCommentActions(): HTMLElement {
const elRoot = <HTMLDivElement>elCommentActionTemplate.cloneNode(true)
this.actionsPopoverReferences = {
elContentRoot: elRoot
}
this.actionsPopoverReferences = { elContentRoot: elRoot }
const elAnchorEcd =
<HTMLAnchorElement>
elRoot.getElementsByClassName('expand-comments-down')[0]
elRoot.getElementsByClassName('expand-comments-down').item(0)
if (this.elPrevCommentLink) {
elAnchorEcd.getElementsByClassName('icon')[0]
.replaceWith(getSvgIcon(mdiArrowLeft))
elAnchorEcd.getElementsByClassName('icon').item(0)
?.replaceWith(getSvgIcon(mdiArrowLeft))
this.actionsPopoverReferences.elExpandCommentsDown = elAnchorEcd
} else elAnchorEcd.remove()
const elAnchorEcu =
<HTMLAnchorElement>
elRoot.getElementsByClassName('expand-comments-up')[0]
elRoot.getElementsByClassName('expand-comments-up').item(0)
const elAnchorEcur =
<HTMLAnchorElement>
elRoot.getElementsByClassName('expand-comments-up-recursive')[0]
elRoot.getElementsByClassName('expand-comments-up-recursive').item(0)
if (this.oneReplies.size) {
elAnchorEcu.getElementsByClassName('icon')[0]
.replaceWith(getSvgIcon(mdiArrowRight))
elAnchorEcur.getElementsByClassName('icon')[0]
.replaceWith(getSvgIcon(myArrowRightDouble))
elAnchorEcu.getElementsByClassName('icon').item(0)
?.replaceWith(getSvgIcon(mdiArrowRight))
elAnchorEcur.getElementsByClassName('icon').item(0)
?.replaceWith(getSvgIcon(myArrowRightDouble))
this.actionsPopoverReferences.elExpandCommentsUp = elAnchorEcu
this.actionsPopoverReferences.elExpandCommentsUpRecursive = elAnchorEcur
} else {
@ -254,27 +318,25 @@ export class OneComment {
}
const elAnchorEcit =
<HTMLAnchorElement>
elRoot.getElementsByClassName('expand-comments-in-thread')[0]
elRoot.getElementsByClassName('expand-comments-in-thread').item(0)
if (this.oneReplies.size || this.elPrevCommentLink) {
elAnchorEcit.getElementsByClassName('icon')[0]
.replaceWith(getSvgIcon(mdiArrowLeftRight))
elAnchorEcit.getElementsByClassName('icon').item(0)
?.replaceWith(getSvgIcon(mdiArrowLeftRight))
this.actionsPopoverReferences.elExpandCommentsInThread = elAnchorEcit
} else elAnchorEcit.remove()
this.initPopoverLinks()
return elRoot
}
private initializeActionsPopover(prevCommentLink?: HTMLAnchorElement): void {
if (this.oneReplies.size === 0 && !prevCommentLink) {
this.elBtnCommentActions.remove()
private initializeActionsPopover(): void {
if (this.oneReplies.size === 0 && !this.elPrevCommentLink) {
this.elBtnCommentActions.hidden = true
return
}
this.elBtnCommentActions.replaceChildren(elSvgCog.cloneNode(true))
popoverAdd(this.elBtnCommentActions, {
popoverOpts: {
content: el => {
return this.getContentCommentActions(el)
},
content: getContentCommentActions,
html: true,
delay: { show: 500, hide: 500 },
sanitize: false,
@ -282,124 +344,51 @@ export class OneComment {
container: <HTMLDivElement>this.elBtnCommentActions.parentElement
},
callbacks: {
onTipInserted: el => {
this.onPopoverTipInsertedCommentAction(el)
},
onTipRemoved: el => {
this.onPopoverTipRemovedCommentAction(el)
}
onTipInserted: onPopoverTipInsertedCommentAction,
onTipRemoved: onPopoverTipRemovedCommentAction
}
})
}
private initializeCallbacks(): void {
private initializePrevComment(
el: HTMLAnchorElement, onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): void {
el.addEventListener('mouseenter', commentHighlightOn)
el.addEventListener('mouseleave', commentHighlightOff)
el.addEventListener('focusin', commentHighlightOn)
el.addEventListener('focusout', commentHighlightOff)
el.addEventListener('click', onClickJumpToComment)
this.elPrevCommentLink = el
}
private initializeCallbacks(
aLink: string, onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): void {
this.elAnchorCommentNumber =
<HTMLAnchorElement>
this.rootEl.getElementsByClassName('comment-number')[0]
this.elAnchorCommentNumber.href =
this.boundFunctions.getALink(this.commentPk)
this.elAnchorCommentNumber.addEventListener(
'click', this.boundFunctions.onClickCommentNumber)
this.elRoot.getElementsByClassName('comment-number').item(0)
this.elAnchorCommentNumber.href = aLink
this.elAnchorCommentNumber.addEventListener('click', onClickCommentNumber)
const prevCommentLink = <HTMLAnchorElement | undefined>
this.rootEl.getElementsByClassName('forum-comment-prevcomment')[0]
this.elRoot.getElementsByClassName('forum-comment-prevcomment').item(0)
if (prevCommentLink instanceof HTMLAnchorElement) {
this.initializePrevComment(prevCommentLink)
this.initializePrevComment(
prevCommentLink, onClickJumpToComment, commentHighlightOn,
commentHighlightOff)
}
const replies = <HTMLCollectionOf<HTMLDivElement>>
this.rootEl.getElementsByClassName('forum-topic-comment-onereply')
this.elRoot.getElementsByClassName('forum-topic-comment-onereply')
for (const oneReplyEl of replies) {
const commentPk = this.initializeOneReply(oneReplyEl)
const commentPk = this.initializeOneReply(
oneReplyEl, onClickJumpToComment, commentHighlightOn,
commentHighlightOff)
this.oneReplies.set(commentPk, oneReplyEl)
}
this.initializeActionsPopover(prevCommentLink)
}
private teardownVotingValue(): void {
const elRoot = this.rootEl.getElementsByClassName('voting-value')[0]
if (!(elRoot instanceof HTMLAnchorElement)) return
votingValueTeardown(elRoot)
}
teardown(): void {
this.elAnchorCommentNumber.removeEventListener(
'click', this.boundFunctions.onClickCommentNumber)
if (this.elPrevCommentLink) {
this.elPrevCommentLink.removeEventListener(
'mouseenter', this.boundFunctions.commentHighlightOn)
this.elPrevCommentLink.removeEventListener(
'mouseleave', this.boundFunctions.commentHighlightOff)
this.elPrevCommentLink.removeEventListener(
'focusin', this.boundFunctions.commentHighlightOn)
this.elPrevCommentLink.removeEventListener(
'focusout', this.boundFunctions.commentHighlightOff)
this.elPrevCommentLink.removeEventListener(
'click', this.boundFunctions.onClickJumpToComment)
}
for (const el of this.oneReplies.values()) this.teardownOneReply(el)
this.oneReplies.clear()
this.teardownVotingValue()
if (this.elBtnCommentActions.parentElement) {
popoverTeardown(this.elBtnCommentActions)
}
}
flashHighlight(): void {
requestAnimationFrame(() => {
this.highlightOn()
requestAnimationFrame(() => {
this.highlightOff()
})
})
}
highlightOn(): void {
this.rootEl.classList.add(highlightedClass)
}
highlightOff(): void {
this.rootEl.classList.remove(highlightedClass)
}
jumpToSelf(onFinishCallback?: () => void): void {
document.addEventListener('scrollend', () => {
if (document.activeElement instanceof HTMLAnchorElement) {
document.activeElement.blur()
}
}, { once: true })
if (onFinishCallback) {
document.addEventListener('scrollend', onFinishCallback, { once: true })
}
scroll({ top: getScrollTop(this.rootEl), behavior: 'auto' })
}
/**
* Will return `true` if at least 1/3 of the comment body is visible.
*/
isInViewport(zeroPoint: number, viewportHeight: number): boolean {
const bodyInfo = this.elBody.getBoundingClientRect()
const relBodyBottomY = bodyInfo.top + bodyInfo.height
// Comment is fully outside of the viewport
if (
relBodyBottomY <= zeroPoint || bodyInfo.top >= viewportHeight
) return false
// Top inside
if (bodyInfo.top >= zeroPoint) return true
// Top outside, bottom inside and above 60% height
if (relBodyBottomY >= viewportHeight * 0.6) return true
// Bottom inside but below 60% height
const relThirdBottomY = bodyInfo.top + bodyInfo.height / 3
// True if the top 1/3 of the comment body is inside
return relThirdBottomY > zeroPoint
}
selectAsFocused(): void {
if (this.rootEl.classList.contains('is-currently-focused')) return
this.rootEl.classList.add('is-currently-focused')
}
deselectAsFocused(): void {
if (!this.rootEl.classList.contains('is-currently-focused')) return
this.rootEl.classList.remove('is-currently-focused')
this.initializeActionsPopover()
}
}
@ -412,6 +401,66 @@ function initCommentActionTemplate(): void {
extractTemplateContent(elCommentActionTemplateStr)
}
function teardownOneReply(
el: HTMLDivElement, onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): void {
const replyLink =
<HTMLAnchorElement>
el.getElementsByClassName('forum-to-reply-comment').item(0)
replyLink.removeEventListener('mouseenter', commentHighlightOn)
replyLink.removeEventListener('mouseleave', commentHighlightOff)
replyLink.removeEventListener('focusin', commentHighlightOn)
replyLink.removeEventListener('focusout', commentHighlightOff)
replyLink.removeEventListener('click', onClickJumpToComment)
}
function teardownVotingValue(obj: OneComment): void {
const elRoot = obj.elRoot.getElementsByClassName('voting-value')[0]
if (!(elRoot instanceof HTMLAnchorElement)) return
votingValueTeardown(elRoot)
}
export function teardownOnecomment(
elRoot: ElOnecommentType, onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): void {
const obj = elRoot.forumOneComment
obj.elAnchorCommentNumber.removeEventListener('click', onClickCommentNumber)
const elPrevComment = obj.elPrevCommentLink
if (elPrevComment) {
elPrevComment.removeEventListener('mouseenter', commentHighlightOn)
elPrevComment.removeEventListener('mouseleave', commentHighlightOff)
elPrevComment.removeEventListener('focusin', commentHighlightOn)
elPrevComment.removeEventListener('focusout', commentHighlightOff)
elPrevComment.removeEventListener('click', onClickJumpToComment)
}
for (const el of obj.oneReplies.values()) {
teardownOneReply(
el, onClickJumpToComment, commentHighlightOn, commentHighlightOff)
}
obj.oneReplies.clear()
teardownVotingValue(obj)
if (!obj.elBtnCommentActions.hidden) {
popoverTeardown(obj.elBtnCommentActions)
}
}
export function initElonecomment(
elRoot: HTMLElement, commentPk: number, aLink: string,
onClickJumpToComment: (ev: MouseEvent) => void,
commentHighlightOn: (ev: Event) => void,
commentHighlightOff: (ev: Event) => void
): ElOnecommentType {
const elRootAdded = <ElOnecommentType>elRoot
elRootAdded.forumOneComment = new OneComment(
elRoot, commentPk, aLink, onClickJumpToComment, commentHighlightOn,
commentHighlightOff)
return elRootAdded
}
export function init(): void {
elSvgCog = getSvgIcon(mdiCogOutline)
initCommentActionTemplate()

View file

@ -74,8 +74,12 @@ export class Paginator {
if (this.isLoading) return
this.isLoading = true
const liEls = this.options.root.getElementsByClassName('page-numbered')
for (const liEl of liEls) liEl.classList.add('disabled')
elClicked?.replaceChildren(loaderTemplate.cloneNode(true))
for (const liEl of liEls) {
liEl.classList.add('disabled')
if (liEl.firstElementChild === elClicked) {
elClicked.replaceChildren(loaderTemplate.cloneNode(true))
}
}
}
unsetLoading(elClicked: HTMLAnchorElement): void {