Intermediate commit, onwards to comment editing
All checks were successful
buildbot/Hondaforum Site Build done.

This commit is contained in:
László Károlyi 2023-12-24 19:34:27 +01:00
parent 04f9e703dc
commit c1b284697e
Signed by: karolyi
GPG key ID: 2DCAF25E55735BFE
13 changed files with 256 additions and 42 deletions

View file

@ -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',
}
},

View file

@ -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 %}

View 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>

View file

@ -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'),
]

View file

@ -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)

View file

@ -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(

View file

@ -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;
}
}

View file

@ -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 })

View file

@ -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')

View file

@ -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
}

View file

@ -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
}

View 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)
}

View file

@ -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