From b7ff19dc5e2fce08618fa792f80639913b46e08c Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 22 Aug 2022 05:11:20 -0400 Subject: [PATCH] comments updates --- app/models/comment.js | 5 +- app/views/comment/components/comment.pug | 14 ++-- app/views/comment/components/composer.pug | 18 +++-- app/views/comment/components/section.pug | 2 +- client/js/index.js | 9 +++ client/js/site-app.js | 15 +--- client/js/site-comments.js | 85 ++++++++++++++--------- 7 files changed, 87 insertions(+), 61 deletions(-) diff --git a/app/models/comment.js b/app/models/comment.js index edc08e8..00b7dad 100644 --- a/app/models/comment.js +++ b/app/models/comment.js @@ -18,6 +18,8 @@ const { RESOURCE_TYPE_LIST, CommentStats, CommentStatsDefaults, + ResourceStats, + ResourceStatsDefaults, } = require(path.join(__dirname, 'lib', 'resource-stats.js')); const COMMENT_STATUS_LIST = [ @@ -40,7 +42,8 @@ const CommentSchema = new Schema({ flags: { isNSFW: { type: Boolean, default: false, required: true }, }, - stats: { type: CommentStats, default: CommentStatsDefaults, required: true }, + resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, + commentStats: { type: CommentStats, default: CommentStatsDefaults, required: true }, }); /* diff --git a/app/views/comment/components/comment.pug b/app/views/comment/components/comment.pug index d85aeac..a3db135 100644 --- a/app/views/comment/components/comment.pug +++ b/app/views/comment/components/comment.pug @@ -28,7 +28,7 @@ mixin renderComment (comment, options) a( href="", data-comment-id= comment._id, - onclick=`return dtp.app.comments['${options.name}'].deleteComment(event);`, + onclick=`return dtp.app.comments.deleteComment(event);`, ) Delete else if user li.uk-nav-header.no-select Moderation menu @@ -38,7 +38,7 @@ mixin renderComment (comment, options) data-resource-type= comment.resourceType, data-resource-id= resourceId, data-comment-id= comment._id, - onclick=`return dtp.app.comments['${options.name}'].showReportCommentForm(event);`, + onclick=`return dtp.app.comments.showReportCommentForm(event);`, ) Report li a( @@ -46,7 +46,7 @@ mixin renderComment (comment, options) data-resource-type= comment.resourceType, data-resource-id= resourceId, data-comment-id= comment._id, - onclick=`return dtp.app.comments['${options.name}'].blockCommentAuthor(event);`, + onclick=`return dtp.app.comments.blockCommentAuthor(event);`, ) Block author .uk-comment-body @@ -82,7 +82,7 @@ mixin renderComment (comment, options) type="button", data-comment-id= comment._id, data-vote="up", - onclick=`return dtp.app.comments['${options.name}'].submitCommentVote(event);`, + onclick=`return dtp.app.comments.submitCommentVote(event);`, title="Upvote this comment", ).uk-button.uk-button-link +renderLabeledIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) @@ -91,7 +91,7 @@ mixin renderComment (comment, options) type="button", data-comment-id= comment._id, data-vote="down", - onclick=`return dtp.app.comments['${options.name}'].submitCommentVote(event);`, + onclick=`return dtp.app.comments.submitCommentVote(event);`, title="Downvote this comment", ).uk-button.uk-button-link +renderLabeledIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) @@ -99,7 +99,7 @@ mixin renderComment (comment, options) button( type="button", data-comment-id= comment._id, - onclick=`return dtp.app.comments['${options.name}'].openReplies(event);`, + onclick=`return dtp.app.comments.openReplies(event);`, title="Load replies to this comment", ).uk-button.uk-button-link +renderLabeledIcon('fa-comment', formatCount(comment.stats.replyCount)) @@ -107,7 +107,7 @@ mixin renderComment (comment, options) button( type="button", data-comment-id= comment._id, - onclick=`return dtp.app.comments['${options.name}'].openReplyComposer(event);`, + onclick=`return dtp.app.comments.openReplyComposer(event);`, title="Write a reply to this comment", ).uk-button.uk-button-link +renderLabeledIcon('fa-reply', 'reply') diff --git a/app/views/comment/components/composer.pug b/app/views/comment/components/composer.pug index 98dfeea..d55e0b3 100644 --- a/app/views/comment/components/composer.pug +++ b/app/views/comment/components/composer.pug @@ -1,5 +1,10 @@ -mixin renderCommentComposer (options = { }) - form(method="POST", action= options.rootUrl, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form +mixin renderCommentComposer (formId, options = { }) + form( + id= formId, + method="POST", + action= options.rootUrl, + onsubmit="return dtp.app.submitForm(event, 'create-comment');", + ).uk-form if options.replyTo input(type="hidden", name="replyTo", value= options.replyTo) @@ -11,7 +16,8 @@ mixin renderCommentComposer (options = { }) rows="4", maxlength="3000", placeholder="Enter comment", - oninput=`return dtp.app.comments['${options.name}'].onCommentInput(event);`, + data-form-id= formId, + oninput=`return dtp.app.comments.onCommentInput(event);`, ).uk-textarea.uk-resize-vertical .uk-text-small div(uk-grid).uk-flex-between @@ -22,15 +28,15 @@ mixin renderCommentComposer (options = { }) div(uk-grid).uk-flex-between.uk-grid-small .uk-width-expand ul.uk-subnav - li + li.comment-emoji-picker button( type="button", uk-tooltip="Add an emoji", ).uk-button.dtp-button-default span i.far.fa-smile - #comment-emoji-picker(uk-drop={ mode: 'click' }) - .comment-emoji-picker + .comment-emoji-picker-drop(data-form-id= formId, uk-drop={ mode: 'click' }) + .comment-emoji-picker-ui div THIS IS THE EMOJI PICKER li(title="Not Safe For Work will hide your comment text by default") diff --git a/app/views/comment/components/section.pug b/app/views/comment/components/section.pug index bc3accf..b60e6db 100644 --- a/app/views/comment/components/section.pug +++ b/app/views/comment/components/section.pug @@ -12,7 +12,7 @@ mixin renderCommentSection (options = { }) .uk-margin +renderSectionTitle('Add a comment') .uk-margin-small - +renderCommentComposer(composerOptions) + +renderCommentComposer(composerOptions.name, composerOptions) if featuredComment .content-block(dtp-comments= `${options.name}-feature`, data-root-url= options.rootUrl) diff --git a/client/js/index.js b/client/js/index.js index fc6399b..0be660f 100644 --- a/client/js/index.js +++ b/client/js/index.js @@ -12,6 +12,15 @@ import DtpSiteApp from './site-app.js'; import DtpWebLog from 'dtp/dtp-log.js'; import UIkit from 'uikit'; +/** + * Monkeypatch to count characters instead of .length's code point count. + * @returns character count of string + */ +String.prototype.charCount = function () { + const splat = [...this]; + return splat.length; +}; + window.addEventListener('load', async ( ) => { // application console log dtp.log = new DtpWebLog(DTP_COMPONENT); diff --git a/client/js/site-app.js b/client/js/site-app.js index 0763554..506c688 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -43,26 +43,13 @@ export default class DtpSiteApp extends DtpApp { }); this.chat = new SiteChat(this); - - this.initializeComments(); + this.comments = new SiteComments(this); this.charts = { /* will hold rendered charts */ }; this.scrollToHash(); } - initializeComments ( ) { - this.comments = { }; - - const containers = document.querySelectorAll('[dtp-comments]'); - containers.forEach((container) => { - const name = container.getAttribute('dtp-comments'); - const rootUrl = container.getAttribute('data-root-url'); - this.log.info('initializeComments', 'initializing commenting scope', { name, rootUrl }); - this.comments[name] = new SiteComments(this, container); - }); - } - async scrollToHash ( ) { const { hash } = window.location; if (hash === '') { diff --git a/client/js/site-comments.js b/client/js/site-comments.js index 3f020e7..fe12dc3 100644 --- a/client/js/site-comments.js +++ b/client/js/site-comments.js @@ -12,38 +12,56 @@ import * as picmo from 'picmo'; export default class SiteComments { - constructor (app, rootElement) { + constructor (app) { this.app = app; this.log = new DtpLog({ name: 'Site Comments', slug: 'comments' }); + 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'); - this.ui = { - input: rootElement.querySelector('#comment-content'), - emojiPicker: rootElement.querySelector('#comment-emoji-picker'), - characterCount: rootElement.querySelector('.comment-character-count'), - }; + 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'); - if (this.ui.emojiPicker) { - this.ui.emojiPickerUI = this.ui.emojiPicker.querySelector('.comment-emoji-picker'); - this.ui.picmo = picmo.createPicker({ + picker.picmo = picmo.createPicker({ emojisPerRow: 7, - rootElement: this.ui.emojiPickerUI, + rootElement: picker.ui, theme: picmo.darkTheme, }); - this.ui.picmo.addEventListener('emoji:select', this.onEmojiSelected.bind(this)); + picker.picmo.addEventListener('emoji:select', this.onEmojiSelected.bind(this, picker)); - this.ui.emojiPickerDrop = UIkit.drop(this.ui.emojiPicker); - UIkit.util.on(this.ui.emojiPicker, 'show', ( ) => { + picker.emojiPickerDrop = UIkit.drop(picker.drop); + UIkit.util.on(picker.drop, 'show', ( ) => { this.log.info('SiteComments', 'showing emoji picker'); - this.ui.picmo.reset(); + picker.picmo.reset(); }); - } else { - UIkit.modal.alert('Comment section without an emoji picker defined'); + + container.setAttribute('data-initialized', true); } } async onCommentInput (event) { - this.ui.characterCount.textContent = numeral(event.target.value.length).format('0,0'); + 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) { @@ -177,6 +195,7 @@ export default class SiteComments { 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}`); } @@ -209,36 +228,38 @@ export default class SiteComments { 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 (event) { - this.ui.emojiPickerDrop.hide(false); - return this.insertContentAtCursor(event.emoji); + 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 (content) { - this.ui.input.focus(); + async insertContentAtCursor (picker, content) { + picker.input.focus(); if (document.selection) { let sel = document.selection.createRange(); sel.text = content; - } else if (this.ui.input.selectionStart || (this.ui.input.selectionStart === 0)) { - let startPos = this.ui.input.selectionStart; - let endPos = this.ui.input.selectionEnd; + } else if (picker.input.selectionStart || (picker.input.selectionStart === 0)) { + let startPos = picker.input.selectionStart; + let endPos = picker.input.selectionEnd; - let oldLength = this.ui.input.value.length; - this.ui.input.value = - this.ui.input.value.substring(0, startPos) + + let oldLength = picker.input.value.length; + picker.input.value = + picker.input.value.substring(0, startPos) + content + - this.ui.input.value.substring(endPos, this.ui.input.value.length); + picker.input.value.substring(endPos, picker.input.value.length); - this.ui.input.selectionStart = startPos + (this.ui.input.value.length - oldLength); - this.ui.input.selectionEnd = this.ui.input.selectionStart; + picker.input.selectionStart = startPos + (picker.input.value.length - oldLength); + picker.input.selectionEnd = picker.input.selectionStart; } else { - this.ui.input.value += content; + picker.input.value += content; } } } \ No newline at end of file