Intermediate commit, onwards to comment editing
All checks were successful
buildbot/Hondaforum Site Build done.
All checks were successful
buildbot/Hondaforum Site Build done.
This commit is contained in:
parent
04f9e703dc
commit
c1b284697e
13 changed files with 256 additions and 42 deletions
|
@ -3,55 +3,58 @@
|
|||
commentPk: {{ comment_pk|default(None)|tojson }},
|
||||
scrollToPk: {{ scroll_to_pk|default(None)|tojson }},
|
||||
listingMode: '{{ listing_mode|default('commentListing') }}',
|
||||
{%- if not request.user.is_anonymous %}
|
||||
editorTemplateUrl: '{{ url('forum:rest-api:base-v1:editor-template')|escapejs }}',
|
||||
{%- endif %}
|
||||
selectors: {
|
||||
root: '{{ root|escapejs }}',
|
||||
commentWrapper: 'topic-comment-wrapper',
|
||||
},
|
||||
urls: {
|
||||
commentListing: {
|
||||
backend: '{{ url('forum:base:topic-comment-listing', topic_slug='example-slug', comment_pk=654321) }}',
|
||||
backend: '{{ url('forum:base:topic-comment-listing', topic_slug='example-slug', comment_pk=654321)|escapejs }}',
|
||||
exampleSlug: 'example-slug',
|
||||
commentPk: '654321'
|
||||
},
|
||||
{%- if listing_mode == 'commentListing' -%}
|
||||
{%- if listing_mode == 'commentListing' %}
|
||||
commentListingPageNo: {
|
||||
backend: '{{ url('forum:base:topic-comment-listing', topic_slug='example-slug') }}?page=654321',
|
||||
backend: '{{ url('forum:base:topic-comment-listing', topic_slug='example-slug')|escapejs }}?page=654321',
|
||||
exampleSlug: 'example-slug',
|
||||
pageId: '654321'
|
||||
},
|
||||
{%- endif %}
|
||||
{%- if not request.user.is_anonymous -%}
|
||||
{%- if not request.user.is_anonymous %}
|
||||
getVotingValueDetails: {
|
||||
backend: '{{ url('forum:rest-api:rating-v1:voting-value-details', comment_pk=76543) }}',
|
||||
backend: '{{ url('forum:rest-api:rating-v1:voting-value-details', comment_pk=76543)|escapejs }}',
|
||||
commentPk: '76543',
|
||||
},
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
expandCommentsDown: {
|
||||
backend: '{{ url('forum:base:comments-down', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890) }}',
|
||||
backend: '{{ url('forum:base:comments-down', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890)|escapejs }}',
|
||||
exampleSlug: 'example-slug',
|
||||
commentPk: '12345',
|
||||
scrollToPk: '67890'
|
||||
},
|
||||
expandCommentsUp: {
|
||||
backend: '{{ url('forum:base:comments-up', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890) }}',
|
||||
backend: '{{ url('forum:base:comments-up', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890)|escapejs }}',
|
||||
exampleSlug: 'example-slug',
|
||||
commentPk: '12345',
|
||||
scrollToPk: '67890'
|
||||
},
|
||||
expandCommentsUpRecursive: {
|
||||
backend: '{{ url('forum:base:comments-up-recursive', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890) }}',
|
||||
backend: '{{ url('forum:base:comments-up-recursive', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890)|escapejs }}',
|
||||
exampleSlug: 'example-slug',
|
||||
commentPk: '12345',
|
||||
scrollToPk: '67890'
|
||||
},
|
||||
expandCommentsEntireThread: {
|
||||
backend: '{{ url('forum:base:comments-entire-thread', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890) }}',
|
||||
backend: '{{ url('forum:base:comments-entire-thread', topic_slug='example-slug', comment_pk=12345, scroll_to_pk=67890)|escapejs }}',
|
||||
exampleSlug: 'example-slug',
|
||||
commentPk: '12345',
|
||||
scrollToPk: '67890'
|
||||
},
|
||||
getTopicSlugByPk: {
|
||||
backend: '{{ url('forum:rest-api:base-v1:get-topicslug-by-commentpk', comment_pk=76543) }}',
|
||||
backend: '{{ url('forum:rest-api:base-v1:get-topicslug-by-commentpk', comment_pk=76543)|escapejs }}',
|
||||
commentPk: '76543',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
{%- if request.user.is_authenticated %}
|
||||
<button class="btn btn-outline-secondary btn-sm reply-to-this" data-bs-toggle="tooltip" title="{{ _('Reply to this comment') }}" href="" tabindex="0">
|
||||
<i class="fa fa-fw fa-reply" aria-hidden="true"></i>
|
||||
<i class="icon" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">{{ _('Reply to this comment') }}</span>
|
||||
</button>
|
||||
{%- endif %}
|
||||
|
|
40
backend/forum/base/jinja/base/text-editor.html
Normal file
40
backend/forum/base/jinja/base/text-editor.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div id="forum-texteditor" class="p-1 sticky-bottom">
|
||||
<div class="forum-texteditor-inner">
|
||||
<div class="tabmenu clearfix">
|
||||
<ul id="forum-texteditor-navtabs" class="nav nav-tabs float-start">
|
||||
<li class="nav-item">
|
||||
<a class="flex-sm-fill text-sm-center nav-link active" id="forum-texteditor-tab-textinput" role="tab" href="#" data-bs-toggle="tab" data-bs-target="#forum-texteditor-tabpane-textinput" aria-controls="forum-texteditor-tabpane-textinput" aria-selected="true">
|
||||
{{ _('Editor') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="flex-sm-fill text-sm-center nav-link" id="forum-texteditor-tab-preview" role="tab" href="#" data-bs-toggle="tab" data-bs-target="#forum-texteditor-tabpane-preview" aria-controls="forum-texteditor-tabpane-preview" aria-selected="false">
|
||||
{{ _('Text preview') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="float-end pe-2">
|
||||
<button id="forum-texteditor-close" type="button" class="btn-close" aria-label="{{ _('Close editor') }}"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="forum-texteditor-tabpane-textinput" role="tabpanel" aria-labelledby="forum-texteditor-tab-textinput">
|
||||
<textarea id="forum-texteditor-textarea"></textarea>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="forum-texteditor-tabpane-preview" role="tabpanel" aria-labelledby="forum-texteditor-tab-preview">
|
||||
STUFF<br>
|
||||
STUFF<br>
|
||||
STUFF<br>
|
||||
STUFF<br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button type="button" class="btn btn-sm btn-secondary">
|
||||
{{ _('Clear textarea') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary">
|
||||
{{ _('Submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
from django.urls.conf import path, re_path
|
||||
|
||||
from .views.api import (
|
||||
ArchivedTopicsStartView, GetTopicSlugFromCommentpkView, TopicListPageView,
|
||||
ArchivedTopicsStartView, EditorTemplateView, GetTopicSlugFromCommentpkView, TopicListPageView,
|
||||
v1_find_users_by_name)
|
||||
from .views.frontend import (
|
||||
DoNotScrapeView, ReloadAccessDataView, TopicCommentListingView,
|
||||
|
@ -68,4 +68,7 @@ urlpatterns_api_base_v1 = [
|
|||
route='topicslug-by-commentpk/<int:comment_pk>/',
|
||||
view=GetTopicSlugFromCommentpkView.as_view(),
|
||||
name='get-topicslug-by-commentpk'),
|
||||
path(
|
||||
route='editor-template/', view=EditorTemplateView.as_view(),
|
||||
name='editor-template'),
|
||||
]
|
||||
|
|
|
@ -134,3 +134,14 @@ def v1_find_users_by_name(request: HttpRequest) -> JsonResponse:
|
|||
'more': page.has_next()}}
|
||||
result['results'] = [{'id': x.slug, 'text': x.username} for x in page]
|
||||
return JsonResponse(data=result)
|
||||
|
||||
|
||||
class EditorTemplateView(TemplateView):
|
||||
'Serving a template for editing text.'
|
||||
http_method_names = ['get']
|
||||
template_name = 'base/text-editor.html'
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs):
|
||||
if not isinstance(request.user, User):
|
||||
raise Http404
|
||||
return super().get(request=request, *args, **kwargs)
|
||||
|
|
|
@ -79,14 +79,14 @@ class VotingValueDetailsView(TemplateView):
|
|||
self.votes.pop(user)
|
||||
|
||||
def get(
|
||||
self, *args, request: HttpRequest, comment_pk: int,
|
||||
self, request: HttpRequest, *args, comment_pk: int,
|
||||
cast_value: str | None, **kwargs) -> HttpResponse:
|
||||
'Only accept a `HTTP GET` when there is NO `cast_value` param.'
|
||||
if cast_value or not isinstance(request.user, User):
|
||||
raise Http404
|
||||
self._comment_pk = comment_pk
|
||||
return super().get(
|
||||
*args, request=request, comment_pk=comment_pk,
|
||||
request=request, *args, comment_pk=comment_pk,
|
||||
cast_value=cast_value, **kwargs)
|
||||
|
||||
def post(
|
||||
|
|
|
@ -396,3 +396,18 @@ svg.forum-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#forum-texteditor {
|
||||
backdrop-filter: blur(.2rem);
|
||||
background-color: transparentize($color: $body-bg, $amount: .2);
|
||||
min-height: 40vh;
|
||||
border-top: var(--bs-border-width) solid var(--bs-border-color);
|
||||
#forum-texteditor-textarea {
|
||||
min-height: 30vh;
|
||||
min-width: 50%;
|
||||
}
|
||||
#forum-texteditor-tabpane-preview {
|
||||
max-height: 30vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ interface TopicCommentListingOptionsType {
|
|||
commentPk?: number
|
||||
scrollToPk?: number
|
||||
listingMode: ListingModeType
|
||||
editorTemplateUrl?: string
|
||||
selectors: {
|
||||
root: string
|
||||
commentWrapper: string
|
||||
|
@ -324,8 +325,8 @@ class TopicCommentListing {
|
|||
if (commentPk === -1) throw new Error('forumLinkTo not found')
|
||||
const aLink = getALink(this, commentPk)
|
||||
const elOneComment = initElonecomment(
|
||||
el, commentPk, aLink, onClickJumpToComment, commentHighlightOn,
|
||||
commentHighlightOff)
|
||||
el, commentPk, aLink, onClickJumpToComment,
|
||||
commentHighlightOn, commentHighlightOff)
|
||||
addOnremoveCallback(elOneComment, onTeardownOnecomment)
|
||||
this.comments.set(commentPk, elOneComment)
|
||||
}
|
||||
|
@ -353,7 +354,7 @@ export function init(options: TopicCommentListingOptionsType): void {
|
|||
throw new Error(`root selector '${options.selectors.root}' not found`)
|
||||
}
|
||||
elRoot = elAssignedRoot
|
||||
oneCommentInit()
|
||||
oneCommentInit(options.editorTemplateUrl)
|
||||
initializeUrls(options)
|
||||
elRoot.forumTopicCommentListing = new TopicCommentListing(options)
|
||||
addEventListener('scroll', onScrollFirst, { once: true })
|
||||
|
|
|
@ -74,8 +74,7 @@ function getElRoot(el: Element): TopicGroupRootElType {
|
|||
|
||||
function showLoader(elRoot: TopicGroupRootElType, on: boolean): void {
|
||||
const wrappers = elRoot.forumTopicGroup.wrappers
|
||||
const elItems =
|
||||
wrappers.topicList.getElementsByClassName('topic-items')
|
||||
const elItems = wrappers.topicList.getElementsByClassName('topic-items')
|
||||
if (on) {
|
||||
for (const el of elItems) el.classList.add('effect-blur')
|
||||
wrappers.loadOverlay?.removeAttribute('hidden')
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { getScrollTop, getSvgIcon, extractTemplateContent } from './common.ts'
|
||||
import {
|
||||
getScrollTop, getSvgIcon, extractTemplateContent, loaderTemplate
|
||||
} from './common.ts'
|
||||
import Tooltip from 'bootstrap/js/src/tooltip.js'
|
||||
import {
|
||||
type ForumPopoverEl, add as popoverAdd, teardown as popoverTeardown
|
||||
|
@ -11,11 +13,13 @@ import {
|
|||
} from './comment-voting-details.ts'
|
||||
import { urlTemplates, stringsPassed } from '../topic-comment-listing.ts'
|
||||
import {
|
||||
mdiArrowLeft, mdiArrowLeftRight, mdiArrowRight, mdiCogOutline
|
||||
mdiArrowLeft, mdiArrowLeftRight, mdiArrowRight, mdiCogOutline, mdiReply
|
||||
} from '@mdi/js'
|
||||
|
||||
let elSvgCog: SVGElement
|
||||
let elCommentActionTemplate: HTMLDivElement
|
||||
let isReplyButtonInPopover: boolean = false
|
||||
let editorUrl: string | undefined
|
||||
const highlightedClass = 'is-highlighted'
|
||||
const myArrowRightDouble =
|
||||
'm 4,11 v 2 h 7.5 l -5.2899702,5.5 1.42,1.42 L 14.27,13 H 16 l ' +
|
||||
|
@ -28,6 +32,7 @@ interface ActionsPopoverReferencesType {
|
|||
elExpandCommentsUp?: HTMLAnchorElement
|
||||
elExpandCommentsUpRecursive?: HTMLAnchorElement
|
||||
elExpandCommentsInThread?: HTMLAnchorElement
|
||||
elReplyToThis?: HTMLButtonElement
|
||||
}
|
||||
|
||||
export type ElOnecommentType = HTMLElement & {
|
||||
|
@ -49,6 +54,29 @@ function getContentCommentActions(
|
|||
return obj.getContentCommentActions()
|
||||
}
|
||||
|
||||
function onClickReplyToThis(this: HTMLButtonElement, ev: MouseEvent): void {
|
||||
ev.preventDefault()
|
||||
const obj = getElRoot(this).forumOneComment
|
||||
this.getElementsByClassName('forum-icon').item(0)
|
||||
?.replaceWith(loaderTemplate.cloneNode(true))
|
||||
Tooltip.getInstance(this)?.hide()
|
||||
this.blur()
|
||||
this.disabled = true
|
||||
if (editorUrl == null) throw new Error('editorUrl is undefined')
|
||||
void (async function () {
|
||||
const [{ init }, response] = await Promise.all([
|
||||
import('./text-editor.ts'), fetch(editorUrl)
|
||||
])
|
||||
if (!response.ok) {
|
||||
throw new Error(`${editorUrl} response was "${response.statusText}"`)
|
||||
}
|
||||
await init(await response.text())
|
||||
obj.elBtnCommentActions.forumPopoverConfig.popoverObj.hide()
|
||||
})().catch(e => {
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
function onPopoverTipInsertedCommentAction(
|
||||
el: ForumPopoverEl<HTMLButtonElement>
|
||||
): void {
|
||||
|
@ -157,7 +185,7 @@ export class OneComment {
|
|||
elAnchorCommentNumber!: HTMLAnchorElement
|
||||
elPrevCommentLink?: HTMLAnchorElement
|
||||
readonly oneReplies = new Map<number, HTMLDivElement>()
|
||||
readonly elBtnCommentActions: HTMLButtonElement
|
||||
readonly elBtnCommentActions: ForumPopoverEl<HTMLButtonElement>
|
||||
readonly elBody: HTMLDivElement
|
||||
private actionsPopoverReferences?: ActionsPopoverReferencesType
|
||||
|
||||
|
@ -173,7 +201,7 @@ export class OneComment {
|
|||
<HTMLDivElement>
|
||||
elRoot.getElementsByClassName('card-block').item(0)
|
||||
this.elBtnCommentActions =
|
||||
<HTMLButtonElement>
|
||||
<ForumPopoverEl<HTMLButtonElement>>
|
||||
elRoot.getElementsByClassName('forum-comment-actions').item(0)
|
||||
const aEls = <HTMLCollectionOf<HTMLAnchorElement>>
|
||||
elRoot.getElementsByClassName('forum-username')
|
||||
|
@ -197,7 +225,11 @@ export class OneComment {
|
|||
onPopoverTipInsertedCommentAction(
|
||||
el: ForumPopoverEl<HTMLButtonElement>
|
||||
): void {
|
||||
if (!this.actionsPopoverReferences) return
|
||||
if (!this.actionsPopoverReferences) {
|
||||
throw new Error(
|
||||
'onPopoverTipInsertedCommentAction: actionsPopoverReferences ' +
|
||||
'is undefined!')
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsDown)
|
||||
|
@ -214,28 +246,43 @@ export class OneComment {
|
|||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread)
|
||||
}
|
||||
delete this.actionsPopoverReferences
|
||||
if (this.actionsPopoverReferences.elReplyToThis) {
|
||||
this.actionsPopoverReferences.elReplyToThis.addEventListener(
|
||||
'click', onClickReplyToThis)
|
||||
Tooltip.getOrCreateInstance(
|
||||
this.actionsPopoverReferences.elReplyToThis)
|
||||
}
|
||||
}
|
||||
|
||||
onPopoverTipRemovedCommentAction(
|
||||
el: ForumPopoverEl<HTMLButtonElement>
|
||||
): void {
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsDown) {
|
||||
if (!this.actionsPopoverReferences) {
|
||||
throw new Error(
|
||||
'onPopoverTipRemovedCommentAction: actionsPopoverReferences ' +
|
||||
'is undefined!')
|
||||
}
|
||||
if (this.actionsPopoverReferences.elExpandCommentsDown) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsDown)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUp) {
|
||||
if (this.actionsPopoverReferences.elExpandCommentsUp) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUp)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsUpRecursive) {
|
||||
if (this.actionsPopoverReferences.elExpandCommentsUpRecursive) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsUpRecursive)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences?.elExpandCommentsInThread) {
|
||||
if (this.actionsPopoverReferences.elExpandCommentsInThread) {
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread
|
||||
)?.dispose()
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread)?.dispose()
|
||||
}
|
||||
if (this.actionsPopoverReferences.elReplyToThis) {
|
||||
this.actionsPopoverReferences.elReplyToThis.removeEventListener(
|
||||
'click', onClickReplyToThis)
|
||||
Tooltip.getInstance(
|
||||
this.actionsPopoverReferences.elReplyToThis)?.dispose()
|
||||
}
|
||||
delete this.actionsPopoverReferences
|
||||
}
|
||||
|
@ -324,12 +371,24 @@ export class OneComment {
|
|||
?.replaceWith(getSvgIcon(mdiArrowLeftRight))
|
||||
this.actionsPopoverReferences.elExpandCommentsInThread = elAnchorEcit
|
||||
} else elAnchorEcit.remove()
|
||||
const elReplyToThis =
|
||||
<HTMLButtonElement | undefined>
|
||||
elRoot.getElementsByClassName('reply-to-this').item(0)
|
||||
if (elReplyToThis) {
|
||||
this.actionsPopoverReferences.elReplyToThis = elReplyToThis
|
||||
elReplyToThis.getElementsByClassName('icon').item(0)
|
||||
?.replaceWith(getSvgIcon(mdiReply))
|
||||
}
|
||||
this.initPopoverLinks()
|
||||
return elRoot
|
||||
}
|
||||
|
||||
private initializeActionsPopover(): void {
|
||||
if (this.oneReplies.size === 0 && !this.elPrevCommentLink) {
|
||||
if (
|
||||
this.oneReplies.size === 0 &&
|
||||
!this.elPrevCommentLink &&
|
||||
!isReplyButtonInPopover
|
||||
) {
|
||||
this.elBtnCommentActions.hidden = true
|
||||
return
|
||||
}
|
||||
|
@ -337,6 +396,7 @@ export class OneComment {
|
|||
popoverAdd(this.elBtnCommentActions, {
|
||||
popoverOpts: {
|
||||
content: getContentCommentActions,
|
||||
showOnClick: true,
|
||||
html: true,
|
||||
delay: { show: 500, hide: 500 },
|
||||
sanitize: false,
|
||||
|
@ -397,8 +457,11 @@ function initCommentActionTemplate(): void {
|
|||
<HTMLTemplateElement>
|
||||
document.getElementById('forum-topic-comment-action-template')
|
||||
elCommentActionTemplate = document.createElement('div')
|
||||
elCommentActionTemplate.classList.add('text-center')
|
||||
elCommentActionTemplate.innerHTML =
|
||||
extractTemplateContent(elCommentActionTemplateStr)
|
||||
isReplyButtonInPopover =
|
||||
!!elCommentActionTemplate.getElementsByClassName('reply-to-this').length
|
||||
}
|
||||
|
||||
function teardownOneReply(
|
||||
|
@ -461,7 +524,8 @@ export function initElonecomment(
|
|||
return elRootAdded
|
||||
}
|
||||
|
||||
export function init(): void {
|
||||
export function init(editorUrlTemplate?: string): void {
|
||||
elSvgCog = getSvgIcon(mdiCogOutline)
|
||||
initCommentActionTemplate()
|
||||
editorUrl = editorUrlTemplate
|
||||
}
|
||||
|
|
|
@ -10,16 +10,16 @@ export type ForumPopoverTipEl<ElCls extends HTMLElement> = ElCls & {
|
|||
forumPopoverEl: ForumPopoverEl<ElCls>
|
||||
}
|
||||
|
||||
type ComputedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
delay: { show: number, hide: number }
|
||||
trigger: 'manual'
|
||||
content: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
}
|
||||
|
||||
type PassedPopoverOpts<ElCls extends HTMLElement> =
|
||||
Omit<Partial<Popover.Options>, 'trigger' | 'content'> & {
|
||||
content?: (el: ForumPopoverEl<ElCls>) => string | Element
|
||||
showOnClick?: boolean
|
||||
}
|
||||
|
||||
type ComputedPopoverOpts<ElCls extends HTMLElement> =
|
||||
PassedPopoverOpts<ElCls> & {
|
||||
delay: { show: number, hide: number }
|
||||
trigger: 'manual'
|
||||
}
|
||||
|
||||
interface CallbacksType<ElCls extends HTMLElement> {
|
||||
|
@ -98,6 +98,12 @@ function onTipInserted<ElCls extends HTMLElement>(
|
|||
addOnremoveCallback(elTip, onTipRemoved)
|
||||
}
|
||||
|
||||
function onClickElement(this: HTMLElement, ev: MouseEvent): void {
|
||||
if (!isForumPopoverEl(this)) return
|
||||
ev.preventDefault()
|
||||
showPopoverImmediately(this.forumPopoverConfig)
|
||||
}
|
||||
|
||||
function onPointerEnterTip<ElCls extends HTMLElement>(
|
||||
this: ForumPopoverTipEl<ElCls>
|
||||
): void {
|
||||
|
@ -284,12 +290,16 @@ export function add<ElCls extends HTMLElement>(
|
|||
value: popoverConfig,
|
||||
configurable: true
|
||||
})
|
||||
const elChanged = <ForumPopoverEl<ElCls>>el
|
||||
el.addEventListener('inserted.bs.popover', onTipInserted)
|
||||
el.addEventListener('hidden.bs.popover', onPopoverHidden)
|
||||
el.addEventListener('focusin', onFocusinElement)
|
||||
el.addEventListener('focusout', onFocusoutElement)
|
||||
el.addEventListener('pointerenter', onPointerEnterElement)
|
||||
el.addEventListener('pointerleave', onPointerLeaveElement)
|
||||
if (popoverConfig.computedOpts.showOnClick) {
|
||||
elChanged.addEventListener('click', onClickElement)
|
||||
}
|
||||
}
|
||||
|
||||
export function teardown<ElCls extends HTMLElement>(el: ElCls): void {
|
||||
|
@ -301,6 +311,9 @@ export function teardown<ElCls extends HTMLElement>(el: ElCls): void {
|
|||
el.removeEventListener('focusout', onFocusoutElement)
|
||||
el.removeEventListener('pointerenter', onPointerEnterElement)
|
||||
el.removeEventListener('pointerleave', onPointerLeaveElement)
|
||||
if (el.forumPopoverConfig.computedOpts.showOnClick) {
|
||||
el.removeEventListener('click', onClickElement)
|
||||
}
|
||||
// @ts-expect-error Popover is being destroyed so it's okay
|
||||
delete el.forumPopoverConfig
|
||||
}
|
||||
|
|
65
frontend/src/ts/utils/text-editor.ts
Normal file
65
frontend/src/ts/utils/text-editor.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import Tab from 'bootstrap/js/src/tab.js'
|
||||
import { addOnremoveCallback } from './mutation-observer'
|
||||
|
||||
let elRootEditor: HTMLDivElement | undefined
|
||||
let elNavTabs: HTMLUListElement | undefined
|
||||
let elBtnCloseEditor: HTMLButtonElement | undefined
|
||||
let elTextArea: HTMLTextAreaElement | undefined
|
||||
|
||||
function teardown(el: HTMLDivElement): void {
|
||||
if (elNavTabs) Tab.getInstance(elNavTabs)?.dispose()
|
||||
elRootEditor?.querySelectorAll<HTMLAnchorElement>('a[role=tab]')
|
||||
.forEach(el => {
|
||||
el.removeEventListener('shown.bs.tab', onShownTab)
|
||||
})
|
||||
elRootEditor = undefined
|
||||
elNavTabs = undefined
|
||||
elBtnCloseEditor = undefined
|
||||
elTextArea = undefined
|
||||
}
|
||||
|
||||
function onShownTab(this: HTMLAnchorElement, ev: Event): void {
|
||||
console.debug('onShownTab', this, ev)
|
||||
}
|
||||
|
||||
function onClickCloseEditor(this: HTMLButtonElement, ev: MouseEvent): void {
|
||||
if (elRootEditor) elRootEditor.remove()
|
||||
}
|
||||
|
||||
function initElements(elRoot: HTMLDivElement): void {
|
||||
elRootEditor = elRoot
|
||||
document.body.appendChild(elRootEditor)
|
||||
elNavTabs =
|
||||
<HTMLUListElement>document.getElementById('forum-texteditor-navtabs')
|
||||
elBtnCloseEditor =
|
||||
<HTMLButtonElement>document.getElementById('forum-texteditor-close')
|
||||
Tab.getOrCreateInstance(elNavTabs)
|
||||
addOnremoveCallback(elRootEditor, teardown)
|
||||
elBtnCloseEditor.addEventListener(
|
||||
'click', onClickCloseEditor, { once: true, passive: true })
|
||||
elTextArea =
|
||||
<HTMLTextAreaElement>document.getElementById('forum-texteditor-textarea')
|
||||
elTextArea.focus()
|
||||
}
|
||||
|
||||
function initTabListeners(elRoot: HTMLDivElement): void {
|
||||
elRoot.querySelectorAll<HTMLAnchorElement>('a[role=tab]').forEach(el => {
|
||||
el.addEventListener('shown.bs.tab', onShownTab)
|
||||
})
|
||||
}
|
||||
export async function init(template: string): Promise<void> {
|
||||
const elOld = document.getElementById('forum-texteditor')
|
||||
if (elOld) {
|
||||
elOld.remove()
|
||||
// Wait for the teardown callback to run if there was an old editor
|
||||
await new Promise(requestAnimationFrame)
|
||||
}
|
||||
const elWrapper = document.createElement('div')
|
||||
elWrapper.innerHTML = template
|
||||
if (!(elWrapper.firstElementChild instanceof HTMLDivElement)) {
|
||||
throw new Error(`${template} isn't a HTMLDivElement`)
|
||||
}
|
||||
const elRoot = elWrapper.firstElementChild
|
||||
initElements(elRoot)
|
||||
initTabListeners(elRoot)
|
||||
}
|
|
@ -89,7 +89,7 @@ class Username {
|
|||
this.isModalStarted = true
|
||||
// This is a fix for a very buggy behavior of firefox mobile, where
|
||||
// it doesn't send `PointerEvent`s to be able to distinguish where a
|
||||
// `Tooltip` shouldn't show on opening the user's profile modal:
|
||||
// `Tooltip` shouldn't show on opening a user's profile modal:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#browser_compatibility
|
||||
el.dataset.forumIgnoreNextTooltip = 'true'
|
||||
const userSlug = el.dataset.forumUserslug
|
||||
|
|
Loading…
Reference in a new issue