// site-comments.js // Copyright (C) 2022 DTP Technologies, LLC // License: Apache-2.0 'use strict'; import DtpLog from 'dtp/dtp-log.js'; import UIkit from 'uikit'; import * as picmo from 'picmo'; export default class SiteComments { constructor (app) { this.app = app; this.log = new DtpLog({ logId: 'site-comments', index: 'siteComments', className: 'SiteComments' }); this.createEmojiPickers(); } createEmojiPickers ( ) { const pickerContainers = document.querySelectorAll('li.comment-emoji-picker:not([data-initialized])'); for (const container of pickerContainers) { const picker = { }; picker.drop = container.querySelector('.comment-emoji-picker-drop'); picker.ui = picker.drop.querySelector('.comment-emoji-picker-ui'); const formId = picker.drop.getAttribute('data-form-id'); picker.form = document.querySelector(`form#${formId}`); picker.input = picker.form.querySelector(`textarea[data-form-id=${formId}]`); picker.characterCount = picker.form.querySelector('span.comment-character-count'); picker.picmo = picmo.createPicker({ emojisPerRow: 7, rootElement: picker.ui, theme: picmo.darkTheme, }); picker.picmo.addEventListener('emoji:select', this.onEmojiSelected.bind(this, picker)); picker.emojiPickerDrop = UIkit.drop(picker.drop); UIkit.util.on(picker.drop, 'show', ( ) => { this.log.info('SiteComments', 'showing emoji picker'); picker.picmo.reset(); }); container.setAttribute('data-initialized', true); } } async onCommentInput (event) { const target = event.currentTarget || event.target; const formId = target.getAttribute('data-form-id'); if (!formId) { return; } const form = document.getElementById(formId); if (!form) { return; } const label = form.querySelector('span.comment-character-count'); if (!label) { return; } label.textContent = numeral(event.target.value.charCount()).format('0,0'); } async showReportCommentForm (event) { event.preventDefault(); event.stopPropagation(); const resourceType = event.currentTarget.getAttribute('data-resource-type'); const resourceId = event.currentTarget.getAttribute('data-resource-id'); const commentId = event.currentTarget.getAttribute('data-comment-id'); this.closeCommentDropdownMenu(commentId); try { const response = await fetch('/content-report/comment/form', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ resourceType, resourceId, commentId }), }); if (!response.ok) { throw new Error('failed to load report form'); } const html = await response.text(); this.currentDialog = UIkit.modal.dialog(html); } catch (error) { this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error }); UIkit.modal.alert(`Failed to report comment: ${error.message}`); } return true; } async deleteComment (event) { event.preventDefault(); event.stopPropagation(); const commentId = (event.currentTarget || event.target).getAttribute('data-comment-id'); try { const response = fetch(`/comment/${commentId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Server error'); } this.app.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to delete comment: ${error.message}`); } } async blockCommentAuthor (event) { event.preventDefault(); event.stopPropagation(); const resourceType = event.currentTarget.getAttribute('data-resource-type'); const resourceId = event.currentTarget.getAttribute('data-resource-id'); const commentId = event.currentTarget.getAttribute('data-comment-id'); const actionUrl = this.getCommentActionUrl(resourceType, resourceId, commentId, 'block-author'); this.closeCommentDropdownMenu(commentId); try { this.log.info('blockCommentAuthor', 'blocking comment author', { resourceType, resourceId, commentId }); const response = await fetch(actionUrl, { method: 'POST'}); await this.app.processResponse(response); } catch (error) { this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error }); UIkit.modal.alert(`Failed to block comment author: ${error.message}`); } return true; } closeCommentDropdownMenu (commentId) { const dropdown = document.querySelector(`[data-comment-id="${commentId}"][uk-dropdown]`); UIkit.dropdown(dropdown).hide(false); } getCommentActionUrl (resourceType, resourceId, commentId, action) { switch (resourceType) { case 'Newsletter': return `/newsletter/${resourceId}/comment/${commentId}/${action}`; case 'Page': return `/page/${resourceId}/comment/${commentId}/${action}`; case 'Post': return `/post/${resourceId}/comment/${commentId}/${action}`; default: break; } throw new Error('Invalid resource type for comment operation'); } async submitCommentVote (event) { const target = (event.currentTarget || event.target); const commentId = target.getAttribute('data-comment-id'); const vote = target.getAttribute('data-vote'); try { const response = await fetch(`/comment/${commentId}/vote`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ vote }), }); await this.app.processResponse(response); } catch (error) { UIkit.modal.alert(`Failed to submit vote: ${error.message}`); } } async openReplies (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const commentId = target.getAttribute('data-comment-id'); const container = document.querySelector(`.dtp-reply-list-container[data-comment-id="${commentId}"]`); const replyList = document.querySelector(`ul.dtp-reply-list[data-comment-id="${commentId}"]`); const isOpen = !container.hasAttribute('hidden'); if (isOpen) { container.setAttribute('hidden', ''); while (replyList.firstChild) { replyList.removeChild(replyList.firstChild); } return; } try { const response = await fetch(`/comment/${commentId}/replies`); this.app.processResponse(response); this.createEmojiPickers(); } catch (error) { UIkit.modal.alert(`Failed to load replies: ${error.message}`); } return true; } async openReplyComposer (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const commentId = target.getAttribute('data-comment-id'); const composer = document.querySelector(`.dtp-reply-composer[data-comment-id="${commentId}"]`); composer.toggleAttribute('hidden'); return true; } async loadMoreComments (event) { event.preventDefault(); event.stopPropagation(); const target = event.currentTarget || event.target; const buttonId = target.getAttribute('data-button-id'); const rootUrl = target.getAttribute('data-root-url'); const nextPage = target.getAttribute('data-next-page'); try { const response = await fetch(`${rootUrl}?p=${nextPage}&buttonId=${buttonId}`); await this.app.processResponse(response); this.createEmojiPickers(); } catch (error) { UIkit.modal.alert(`Failed to load more comments: ${error.message}`); } } async onEmojiSelected (picker, event) { picker.emojiPickerDrop.hide(false); await this.insertContentAtCursor(picker, event.emoji); picker.characterCount.textContent = numeral(picker.input.value.charCount()).format('0,0'); } async insertContentAtCursor (picker, content) { picker.input.focus(); if (document.selection) { let sel = document.selection.createRange(); sel.text = content; } else if (picker.input.selectionStart || (picker.input.selectionStart === 0)) { let startPos = picker.input.selectionStart; let endPos = picker.input.selectionEnd; let oldLength = picker.input.value.length; picker.input.value = picker.input.value.substring(0, startPos) + content + picker.input.value.substring(endPos, picker.input.value.length); picker.input.selectionStart = startPos + (picker.input.value.length - oldLength); picker.input.selectionEnd = picker.input.selectionStart; } else { picker.input.value += content; } } }