More removal of bound functions
This commit is contained in:
parent
95cd6d378e
commit
e9df2ae400
7 changed files with 499 additions and 445 deletions
|
@ -39,14 +39,14 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
</section>
|
</section>
|
||||||
<section class="pagination-comments">
|
<section class="pagination-comments-wrapper">
|
||||||
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES) }}
|
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES, extra_classnames='pagination-comments') }}
|
||||||
</section>
|
</section>
|
||||||
{% for comment in page_comments %}
|
{% for comment in page_comments %}
|
||||||
{{ topic_comment_template(comment=comment, topic=topic, is_template=False) }}
|
{{ topic_comment_template(comment=comment, topic=topic, is_template=False) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<section class="pagination-comments">
|
<section class="pagination-comments-wrapper">
|
||||||
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES) }}
|
{{ paginator_generic(page=page_comments, adjacent_pages=django_settings.PAGINATOR_DEFAULT_ADJACENT_PAGES, extra_classnames='pagination-comments') }}
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
|
|
@ -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) %}
|
{% 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 %}
|
{% if page_number_list %}
|
||||||
<ul class="pagination">
|
<ul class="pagination{% if extra_classnames %} {{ extra_classnames }}{% endif%}">
|
||||||
{%- for dict_page in page_number_list %}
|
{%- for dict_page in page_number_list %}
|
||||||
{% if dict_page.type == 'number' -%}
|
{% if dict_page.type == 'number' -%}
|
||||||
<li class="page-item page-numbered
|
<li class="page-item page-numbered
|
||||||
|
|
|
@ -245,7 +245,7 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-comments {
|
.pagination-comments-wrapper {
|
||||||
.pagination {
|
.pagination {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,12 @@ import { type TemplateExecutor } from 'lodash'
|
||||||
import template from 'lodash/template.js'
|
import template from 'lodash/template.js'
|
||||||
import { addOnremoveCallback } from './utils/mutation-observer.ts'
|
import { addOnremoveCallback } from './utils/mutation-observer.ts'
|
||||||
import { getNavbarHeight, promiseWindowLoad } from './utils/common.ts'
|
import { getNavbarHeight, promiseWindowLoad } from './utils/common.ts'
|
||||||
import { Paginator } from './utils/paginator.ts'
|
import { Paginator, type PaginatorRootElType } from './utils/paginator.ts'
|
||||||
import { OneComment, init as oneCommentInit } from './utils/one-comment.ts'
|
import {
|
||||||
|
init as oneCommentInit, initElonecomment, type ElOnecommentType,
|
||||||
|
teardownOnecomment, isInViewport, deselectAsFocused, selectAsFocused,
|
||||||
|
jumpToSelf, flashHighlight, highlightOn, highlightOff
|
||||||
|
} from './utils/one-comment.ts'
|
||||||
|
|
||||||
export interface PassedStringsType {
|
export interface PassedStringsType {
|
||||||
commentUrlCopied: string
|
commentUrlCopied: string
|
||||||
|
@ -72,7 +76,7 @@ interface TopicCommentListingOptionsType {
|
||||||
strings: PassedStringsType
|
strings: PassedStringsType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TopicCommentListingUrltemplateType {
|
export let urlTemplates: {
|
||||||
commentListing: TemplateExecutor
|
commentListing: TemplateExecutor
|
||||||
expandCommentsDown: TemplateExecutor
|
expandCommentsDown: TemplateExecutor
|
||||||
expandCommentsUp: TemplateExecutor
|
expandCommentsUp: TemplateExecutor
|
||||||
|
@ -83,14 +87,6 @@ export interface TopicCommentListingUrltemplateType {
|
||||||
getVotingValueDetails?: TemplateExecutor
|
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
|
export let stringsPassed: PassedStringsType
|
||||||
|
|
||||||
function initializeUrls(options: TopicCommentListingOptionsType): void {
|
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 {
|
class TopicCommentListing {
|
||||||
private readonly options: TopicCommentListingOptionsType
|
readonly options: TopicCommentListingOptionsType
|
||||||
private boundFunctions!: TopicBoundFunctionsType
|
readonly comments = new Map<number, ElOnecommentType>()
|
||||||
private root!: HTMLElement
|
isPageScrolled = false
|
||||||
private readonly comments = new Map<number, OneComment>()
|
lastSelectedCommentPk = 0
|
||||||
private isPageScrolled = false
|
readonly paginators = new Array<Paginator>()
|
||||||
private lastSelectedCommentPk = 0
|
|
||||||
private readonly paginators = new Array<Paginator>()
|
|
||||||
|
|
||||||
constructor (options: TopicCommentListingOptionsType) {
|
constructor (
|
||||||
|
root: ElTopicCommentListingType,
|
||||||
|
options: TopicCommentListingOptionsType
|
||||||
|
) {
|
||||||
this.options = options
|
this.options = options
|
||||||
|
this.initializeElOnecomments(root)
|
||||||
|
this.initializePaginators(root)
|
||||||
|
addEventListener('scrollend', onScrollEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
private commentHighlightOn(event: Event): void {
|
private initializeElOnecomments(root: ElTopicCommentListingType): 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
|
|
||||||
const commentWrappers = <HTMLCollectionOf<HTMLElement>>
|
const commentWrappers = <HTMLCollectionOf<HTMLElement>>
|
||||||
this.root.getElementsByClassName(this.options.selectors.commentWrapper)
|
root.getElementsByClassName(this.options.selectors.commentWrapper)
|
||||||
for (const el of commentWrappers) {
|
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> {
|
private initializePaginators(root: ElTopicCommentListingType): 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 {
|
|
||||||
if (this.options.listingMode !== 'commentListing') return
|
if (this.options.listingMode !== 'commentListing') return
|
||||||
const wrappers =
|
const wrappers =
|
||||||
<HTMLCollectionOf<HTMLElement>>
|
<HTMLCollectionOf<HTMLUListElement>>
|
||||||
this.root.getElementsByClassName('pagination-comments')
|
root.getElementsByClassName('pagination-comments')
|
||||||
for (const elRoot of wrappers) {
|
for (const elRoot of wrappers) {
|
||||||
this.paginators.push(new Paginator({
|
this.paginators.push(Paginator.add({
|
||||||
root: elRoot,
|
root: elRoot, callbacks: { load: onPaginate }
|
||||||
callbacks: {
|
|
||||||
load: this.onPaginate.bind(this)
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export function init(options: TopicCommentListingOptionsType): void {
|
||||||
|
stringsPassed = options.strings
|
||||||
oneCommentInit()
|
oneCommentInit()
|
||||||
initializeUrls(options)
|
initializeUrls(options)
|
||||||
stringsPassed = options.strings
|
const root = getElRoot()
|
||||||
const obj = new TopicCommentListing(options)
|
root.forumTopicCommentListing = new TopicCommentListing(root, options)
|
||||||
obj.initialize()
|
addEventListener('scroll', onScrollFirst, { once: true })
|
||||||
|
initialJumpToComment(root.forumTopicCommentListing).catch((e) => {
|
||||||
|
console.error('initial scroll resulted in failure:', e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,6 +300,10 @@ export function add(elRoot: HTMLAnchorElement, commentPk: number): void {
|
||||||
throw new Error(`VotingValue.add: ${elRoot.outerHTML} already initialized`)
|
throw new Error(`VotingValue.add: ${elRoot.outerHTML} already initialized`)
|
||||||
}
|
}
|
||||||
storedObjs.set(elRoot, { commentPk })
|
storedObjs.set(elRoot, { commentPk })
|
||||||
|
const container = elRoot.parentElement
|
||||||
|
if (!container) {
|
||||||
|
throw new Error(`container of ${elRoot.outerHTML} is not a HTMLElement`)
|
||||||
|
}
|
||||||
popoverAdd(elRoot, {
|
popoverAdd(elRoot, {
|
||||||
callbacks: { onTipInserted, onTipRemoved },
|
callbacks: { onTipInserted, onTipRemoved },
|
||||||
popoverOpts: {
|
popoverOpts: {
|
||||||
|
@ -311,7 +315,7 @@ export function add(elRoot: HTMLAnchorElement, commentPk: number): void {
|
||||||
hide: 250
|
hide: 250
|
||||||
},
|
},
|
||||||
title: stringsPassed.castedVotes,
|
title: stringsPassed.castedVotes,
|
||||||
container: <HTMLElement>elRoot.parentElement,
|
container,
|
||||||
// template: popoverTemplate,
|
// template: popoverTemplate,
|
||||||
customClass: 'voting-value-popover-wrapper'
|
customClass: 'voting-value-popover-wrapper'
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,7 @@ import { add as timeActualizerAdd } from './time-actualizer.ts'
|
||||||
import {
|
import {
|
||||||
add as votingValueAdd, teardown as votingValueTeardown
|
add as votingValueAdd, teardown as votingValueTeardown
|
||||||
} from './comment-voting-details.ts'
|
} from './comment-voting-details.ts'
|
||||||
import {
|
import { urlTemplates, stringsPassed } from '../topic-comment-listing.ts'
|
||||||
type TopicBoundFunctionsType, urlTemplates, stringsPassed
|
|
||||||
} from '../topic-comment-listing.ts'
|
|
||||||
import {
|
import {
|
||||||
mdiArrowLeft, mdiArrowLeftRight, mdiArrowRight, mdiCogOutline
|
mdiArrowLeft, mdiArrowLeftRight, mdiArrowRight, mdiCogOutline
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
|
@ -32,61 +30,171 @@ interface ActionsPopoverReferencesType {
|
||||||
elExpandCommentsInThread?: HTMLAnchorElement
|
elExpandCommentsInThread?: HTMLAnchorElement
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OneCommentBoundFunctionsType extends TopicBoundFunctionsType {
|
export type ElOnecommentType = HTMLElement & {
|
||||||
onClickCommentNumber: (event: MouseEvent) => void
|
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 {
|
export class OneComment {
|
||||||
readonly rootEl: HTMLElement
|
readonly elRoot: HTMLElement
|
||||||
private readonly commentPk: number
|
readonly commentPk: number
|
||||||
private readonly boundFunctions: OneCommentBoundFunctionsType
|
elAnchorCommentNumber!: HTMLAnchorElement
|
||||||
private readonly oneReplies = new Map<number, HTMLDivElement>()
|
elPrevCommentLink?: HTMLAnchorElement
|
||||||
private elPrevCommentLink?: HTMLAnchorElement
|
readonly oneReplies = new Map<number, HTMLDivElement>()
|
||||||
private elAnchorCommentNumber!: HTMLAnchorElement
|
readonly elBtnCommentActions: HTMLButtonElement
|
||||||
private readonly elBody: HTMLDivElement
|
readonly elBody: HTMLDivElement
|
||||||
private readonly elBtnCommentActions: HTMLButtonElement
|
|
||||||
private actionsPopoverReferences?: ActionsPopoverReferencesType
|
private actionsPopoverReferences?: ActionsPopoverReferencesType
|
||||||
|
|
||||||
constructor ({ root, commentPk, boundFunctions }: {
|
constructor (
|
||||||
root: HTMLElement
|
elRoot: HTMLElement, commentPk: number, aLink: string,
|
||||||
commentPk: number
|
onClickJumpToComment: (ev: MouseEvent) => void,
|
||||||
boundFunctions: TopicBoundFunctionsType
|
commentHighlightOn: (ev: Event) => void,
|
||||||
}) {
|
commentHighlightOff: (ev: Event) => void
|
||||||
this.rootEl = root
|
) {
|
||||||
|
this.elRoot = elRoot
|
||||||
this.commentPk = commentPk
|
this.commentPk = commentPk
|
||||||
this.boundFunctions = {
|
this.elBody =
|
||||||
...boundFunctions,
|
<HTMLDivElement>
|
||||||
onClickCommentNumber: this.onClickCommentNumber.bind(this)
|
elRoot.getElementsByClassName('card-block').item(0)
|
||||||
}
|
|
||||||
const aEls = <HTMLCollectionOf<HTMLAnchorElement>>
|
|
||||||
root.getElementsByClassName('forum-username')
|
|
||||||
this.elBody = <HTMLDivElement>root.getElementsByClassName('card-block')[0]
|
|
||||||
this.elBtnCommentActions =
|
this.elBtnCommentActions =
|
||||||
<HTMLButtonElement>
|
<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)
|
for (const aEl of aEls) usernameAdd(aEl)
|
||||||
timeActualizerAdd(
|
timeActualizerAdd(
|
||||||
<HTMLCollectionOf<HTMLTimeElement>>
|
<HTMLCollectionOf<HTMLTimeElement>>
|
||||||
root.getElementsByClassName('forum-time'))
|
elRoot.getElementsByClassName('forum-time'))
|
||||||
this.initVotingValue()
|
this.initVotingValue()
|
||||||
this.initializeCallbacks()
|
this.initializeCallbacks(
|
||||||
|
aLink, onClickJumpToComment, commentHighlightOn, commentHighlightOff)
|
||||||
}
|
}
|
||||||
|
|
||||||
private initVotingValue(): void {
|
private initVotingValue(): void {
|
||||||
const elRoot = this.rootEl.getElementsByClassName('voting-value')[0]
|
const elRoot = this.elRoot.getElementsByClassName('voting-value').item(0)
|
||||||
if (!(elRoot instanceof HTMLAnchorElement)) return
|
if (!(elRoot instanceof HTMLAnchorElement)) {
|
||||||
|
throw new Error('voting value on not found on comment')
|
||||||
|
}
|
||||||
votingValueAdd(elRoot, this.commentPk)
|
votingValueAdd(elRoot, this.commentPk)
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSuccessClipboardCopy(): void {
|
onPopoverTipInsertedCommentAction(
|
||||||
toastAdd({ message: stringsPassed.commentUrlCopied })
|
|
||||||
}
|
|
||||||
|
|
||||||
private onErrorClipboardCopy(): void {
|
|
||||||
toastAdd({ message: stringsPassed.noClipboardError })
|
|
||||||
}
|
|
||||||
|
|
||||||
private onPopoverTipInsertedCommentAction(
|
|
||||||
el: ForumPopoverEl<HTMLButtonElement>
|
el: ForumPopoverEl<HTMLButtonElement>
|
||||||
): void {
|
): void {
|
||||||
if (!this.actionsPopoverReferences) return
|
if (!this.actionsPopoverReferences) return
|
||||||
|
@ -109,7 +217,9 @@ export class OneComment {
|
||||||
delete this.actionsPopoverReferences
|
delete this.actionsPopoverReferences
|
||||||
}
|
}
|
||||||
|
|
||||||
private onPopoverTipRemovedCommentAction(el: HTMLButtonElement): void {
|
onPopoverTipRemovedCommentAction(
|
||||||
|
el: ForumPopoverEl<HTMLButtonElement>
|
||||||
|
): void {
|
||||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||||
Tooltip.getInstance(
|
Tooltip.getInstance(
|
||||||
this.actionsPopoverReferences.elExpandCommentsDown)?.dispose()
|
this.actionsPopoverReferences.elExpandCommentsDown)?.dispose()
|
||||||
|
@ -130,69 +240,27 @@ export class OneComment {
|
||||||
delete this.actionsPopoverReferences
|
delete this.actionsPopoverReferences
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClickCommentNumber(event: MouseEvent): void {
|
private initializeOneReply(
|
||||||
const commentLink = event.currentTarget
|
elOneReply: HTMLDivElement, onClickJumpToComment: (ev: MouseEvent) => void,
|
||||||
if (!(commentLink instanceof HTMLAnchorElement)) return
|
commentHighlightOn: (ev: Event) => void,
|
||||||
event.preventDefault()
|
commentHighlightOff: (ev: Event) => void
|
||||||
if (navigator.clipboard == null) {
|
): number {
|
||||||
this.onErrorClipboardCopy()
|
const el =
|
||||||
return
|
<HTMLAnchorElement>
|
||||||
}
|
elOneReply.getElementsByClassName('forum-to-reply-comment')[0]
|
||||||
navigator.clipboard.writeText(commentLink.href).then(
|
const commentPk = parseInt(el.dataset.forumLinkTo ?? '-1')
|
||||||
this.onSuccessClipboardCopy.bind(this),
|
if (commentPk === -1) throw new Error('forumLinkTo on reply not found')
|
||||||
this.onErrorClipboardCopy.bind(this))
|
this.oneReplies.set(commentPk, elOneReply)
|
||||||
}
|
el.addEventListener('mouseenter', commentHighlightOn)
|
||||||
|
el.addEventListener('mouseleave', commentHighlightOff)
|
||||||
private initializeOneReply(el: HTMLDivElement): number {
|
el.addEventListener('focusin', commentHighlightOn)
|
||||||
const replyLink = <HTMLAnchorElement>
|
el.addEventListener('focusout', commentHighlightOff)
|
||||||
el.getElementsByClassName('forum-to-reply-comment')[0]
|
el.addEventListener('click', onClickJumpToComment)
|
||||||
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)
|
|
||||||
return commentPk
|
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 {
|
private initPopoverLinks(): void {
|
||||||
const topicSlug = this.rootEl.dataset.forumTopicSlug
|
const topicSlug = this.elRoot.dataset.forumTopicSlug
|
||||||
if (typeof topicSlug !== 'string') return
|
if (typeof topicSlug !== 'string') return
|
||||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||||
this.actionsPopoverReferences.elExpandCommentsDown.href =
|
this.actionsPopoverReferences.elExpandCommentsDown.href =
|
||||||
|
@ -220,32 +288,28 @@ export class OneComment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getContentCommentActions(
|
getContentCommentActions(): HTMLElement {
|
||||||
el: ForumPopoverEl<HTMLButtonElement>
|
|
||||||
): HTMLElement {
|
|
||||||
const elRoot = <HTMLDivElement>elCommentActionTemplate.cloneNode(true)
|
const elRoot = <HTMLDivElement>elCommentActionTemplate.cloneNode(true)
|
||||||
this.actionsPopoverReferences = {
|
this.actionsPopoverReferences = { elContentRoot: elRoot }
|
||||||
elContentRoot: elRoot
|
|
||||||
}
|
|
||||||
const elAnchorEcd =
|
const elAnchorEcd =
|
||||||
<HTMLAnchorElement>
|
<HTMLAnchorElement>
|
||||||
elRoot.getElementsByClassName('expand-comments-down')[0]
|
elRoot.getElementsByClassName('expand-comments-down').item(0)
|
||||||
if (this.elPrevCommentLink) {
|
if (this.elPrevCommentLink) {
|
||||||
elAnchorEcd.getElementsByClassName('icon')[0]
|
elAnchorEcd.getElementsByClassName('icon').item(0)
|
||||||
.replaceWith(getSvgIcon(mdiArrowLeft))
|
?.replaceWith(getSvgIcon(mdiArrowLeft))
|
||||||
this.actionsPopoverReferences.elExpandCommentsDown = elAnchorEcd
|
this.actionsPopoverReferences.elExpandCommentsDown = elAnchorEcd
|
||||||
} else elAnchorEcd.remove()
|
} else elAnchorEcd.remove()
|
||||||
const elAnchorEcu =
|
const elAnchorEcu =
|
||||||
<HTMLAnchorElement>
|
<HTMLAnchorElement>
|
||||||
elRoot.getElementsByClassName('expand-comments-up')[0]
|
elRoot.getElementsByClassName('expand-comments-up').item(0)
|
||||||
const elAnchorEcur =
|
const elAnchorEcur =
|
||||||
<HTMLAnchorElement>
|
<HTMLAnchorElement>
|
||||||
elRoot.getElementsByClassName('expand-comments-up-recursive')[0]
|
elRoot.getElementsByClassName('expand-comments-up-recursive').item(0)
|
||||||
if (this.oneReplies.size) {
|
if (this.oneReplies.size) {
|
||||||
elAnchorEcu.getElementsByClassName('icon')[0]
|
elAnchorEcu.getElementsByClassName('icon').item(0)
|
||||||
.replaceWith(getSvgIcon(mdiArrowRight))
|
?.replaceWith(getSvgIcon(mdiArrowRight))
|
||||||
elAnchorEcur.getElementsByClassName('icon')[0]
|
elAnchorEcur.getElementsByClassName('icon').item(0)
|
||||||
.replaceWith(getSvgIcon(myArrowRightDouble))
|
?.replaceWith(getSvgIcon(myArrowRightDouble))
|
||||||
this.actionsPopoverReferences.elExpandCommentsUp = elAnchorEcu
|
this.actionsPopoverReferences.elExpandCommentsUp = elAnchorEcu
|
||||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive = elAnchorEcur
|
this.actionsPopoverReferences.elExpandCommentsUpRecursive = elAnchorEcur
|
||||||
} else {
|
} else {
|
||||||
|
@ -254,27 +318,25 @@ export class OneComment {
|
||||||
}
|
}
|
||||||
const elAnchorEcit =
|
const elAnchorEcit =
|
||||||
<HTMLAnchorElement>
|
<HTMLAnchorElement>
|
||||||
elRoot.getElementsByClassName('expand-comments-in-thread')[0]
|
elRoot.getElementsByClassName('expand-comments-in-thread').item(0)
|
||||||
if (this.oneReplies.size || this.elPrevCommentLink) {
|
if (this.oneReplies.size || this.elPrevCommentLink) {
|
||||||
elAnchorEcit.getElementsByClassName('icon')[0]
|
elAnchorEcit.getElementsByClassName('icon').item(0)
|
||||||
.replaceWith(getSvgIcon(mdiArrowLeftRight))
|
?.replaceWith(getSvgIcon(mdiArrowLeftRight))
|
||||||
this.actionsPopoverReferences.elExpandCommentsInThread = elAnchorEcit
|
this.actionsPopoverReferences.elExpandCommentsInThread = elAnchorEcit
|
||||||
} else elAnchorEcit.remove()
|
} else elAnchorEcit.remove()
|
||||||
this.initPopoverLinks()
|
this.initPopoverLinks()
|
||||||
return elRoot
|
return elRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeActionsPopover(prevCommentLink?: HTMLAnchorElement): void {
|
private initializeActionsPopover(): void {
|
||||||
if (this.oneReplies.size === 0 && !prevCommentLink) {
|
if (this.oneReplies.size === 0 && !this.elPrevCommentLink) {
|
||||||
this.elBtnCommentActions.remove()
|
this.elBtnCommentActions.hidden = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.elBtnCommentActions.replaceChildren(elSvgCog.cloneNode(true))
|
this.elBtnCommentActions.replaceChildren(elSvgCog.cloneNode(true))
|
||||||
popoverAdd(this.elBtnCommentActions, {
|
popoverAdd(this.elBtnCommentActions, {
|
||||||
popoverOpts: {
|
popoverOpts: {
|
||||||
content: el => {
|
content: getContentCommentActions,
|
||||||
return this.getContentCommentActions(el)
|
|
||||||
},
|
|
||||||
html: true,
|
html: true,
|
||||||
delay: { show: 500, hide: 500 },
|
delay: { show: 500, hide: 500 },
|
||||||
sanitize: false,
|
sanitize: false,
|
||||||
|
@ -282,124 +344,51 @@ export class OneComment {
|
||||||
container: <HTMLDivElement>this.elBtnCommentActions.parentElement
|
container: <HTMLDivElement>this.elBtnCommentActions.parentElement
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
onTipInserted: el => {
|
onTipInserted: onPopoverTipInsertedCommentAction,
|
||||||
this.onPopoverTipInsertedCommentAction(el)
|
onTipRemoved: onPopoverTipRemovedCommentAction
|
||||||
},
|
|
||||||
onTipRemoved: el => {
|
|
||||||
this.onPopoverTipRemovedCommentAction(el)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
this.elAnchorCommentNumber =
|
||||||
<HTMLAnchorElement>
|
<HTMLAnchorElement>
|
||||||
this.rootEl.getElementsByClassName('comment-number')[0]
|
this.elRoot.getElementsByClassName('comment-number').item(0)
|
||||||
this.elAnchorCommentNumber.href =
|
this.elAnchorCommentNumber.href = aLink
|
||||||
this.boundFunctions.getALink(this.commentPk)
|
this.elAnchorCommentNumber.addEventListener('click', onClickCommentNumber)
|
||||||
this.elAnchorCommentNumber.addEventListener(
|
|
||||||
'click', this.boundFunctions.onClickCommentNumber)
|
|
||||||
const prevCommentLink = <HTMLAnchorElement | undefined>
|
const prevCommentLink = <HTMLAnchorElement | undefined>
|
||||||
this.rootEl.getElementsByClassName('forum-comment-prevcomment')[0]
|
this.elRoot.getElementsByClassName('forum-comment-prevcomment').item(0)
|
||||||
if (prevCommentLink instanceof HTMLAnchorElement) {
|
if (prevCommentLink instanceof HTMLAnchorElement) {
|
||||||
this.initializePrevComment(prevCommentLink)
|
this.initializePrevComment(
|
||||||
|
prevCommentLink, onClickJumpToComment, commentHighlightOn,
|
||||||
|
commentHighlightOff)
|
||||||
}
|
}
|
||||||
const replies = <HTMLCollectionOf<HTMLDivElement>>
|
const replies = <HTMLCollectionOf<HTMLDivElement>>
|
||||||
this.rootEl.getElementsByClassName('forum-topic-comment-onereply')
|
this.elRoot.getElementsByClassName('forum-topic-comment-onereply')
|
||||||
for (const oneReplyEl of replies) {
|
for (const oneReplyEl of replies) {
|
||||||
const commentPk = this.initializeOneReply(oneReplyEl)
|
const commentPk = this.initializeOneReply(
|
||||||
|
oneReplyEl, onClickJumpToComment, commentHighlightOn,
|
||||||
|
commentHighlightOff)
|
||||||
this.oneReplies.set(commentPk, oneReplyEl)
|
this.oneReplies.set(commentPk, oneReplyEl)
|
||||||
}
|
}
|
||||||
this.initializeActionsPopover(prevCommentLink)
|
this.initializeActionsPopover()
|
||||||
}
|
|
||||||
|
|
||||||
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')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -412,6 +401,66 @@ function initCommentActionTemplate(): void {
|
||||||
extractTemplateContent(elCommentActionTemplateStr)
|
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 {
|
export function init(): void {
|
||||||
elSvgCog = getSvgIcon(mdiCogOutline)
|
elSvgCog = getSvgIcon(mdiCogOutline)
|
||||||
initCommentActionTemplate()
|
initCommentActionTemplate()
|
||||||
|
|
|
@ -74,8 +74,12 @@ export class Paginator {
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const liEls = this.options.root.getElementsByClassName('page-numbered')
|
const liEls = this.options.root.getElementsByClassName('page-numbered')
|
||||||
for (const liEl of liEls) liEl.classList.add('disabled')
|
for (const liEl of liEls) {
|
||||||
elClicked?.replaceChildren(loaderTemplate.cloneNode(true))
|
liEl.classList.add('disabled')
|
||||||
|
if (liEl.firstElementChild === elClicked) {
|
||||||
|
elClicked.replaceChildren(loaderTemplate.cloneNode(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsetLoading(elClicked: HTMLAnchorElement): void {
|
unsetLoading(elClicked: HTMLAnchorElement): void {
|
||||||
|
|
Loading…
Reference in a new issue