var iconSmallThread = ``; /** * * A function that triggers all the interactivity when a page with comments are loaded. * It calls sub-functions to define functionality for fetching async info, and attaching * event listeners related to rich UI interaction and form submission. * * When comment reaction data needs to be fetched and how we make those choices: * When no logged in user only need to fetch reaction numbers if there has been recent comment activity. * If no recent comments, we do not need the precision of fetching latest counts async. Static value is almost certainly good enough. * Always fetch when user is logged in because we need state of whether the user has reacted. * * @function initializeCommentsPage */ function initializeCommentsPage() { if (document.getElementById('comments-container')) { toggleCodeOfConduct(); var userStatus = document.body.getAttribute('data-user-status'); var commentableId = document.getElementById('comments-container').dataset.commentableId; var commentableType = document.getElementById('comments-container').dataset.commentableType; var hasRecentCommentActivity = document.getElementById('comments-container').dataset.hasRecentCommentActivity; commentableIdList = commentableId.split(","); if (userStatus === 'logged-in' || hasRecentCommentActivity !== 'false') { var f = (function() { for (var i = 0; i < commentableIdList.length; i++) { (function(i){ var ajaxReq; ajaxReq = new XMLHttpRequest(); ajaxReq.onreadystatechange = function () { if (ajaxReq.readyState === XMLHttpRequest.DONE) { var responseObj = JSON.parse(ajaxReq.response); var reactions = responseObj.reactions; var allNodes = document.getElementsByClassName('single-comment-node'); var publicReactionCounts = responseObj.public_reaction_counts; for (var i = 0; i < reactions.length; i++) { var buttForComment = document.getElementById('button-for-comment-' + reactions[i].reactable_id); if (buttForComment) { buttForComment.classList.add('reacted'); buttForComment.setAttribute('aria-pressed', 'true') } } for (var i = 0; i < publicReactionCounts.length; i++) { var buttForComment = document.getElementById('button-for-comment-' + publicReactionCounts[i].id); if (buttForComment) { var reactionsCountWrapper = buttForComment.querySelector('.reactions-count'); var reactionsLabelWrapper = buttForComment.querySelector('.reactions-label'); if (publicReactionCounts[i].count > 0 && reactionsLabelWrapper) { if (publicReactionCounts[i].count > 1) { reactionsLabelWrapper.innerHTML = ` ${I18n.t('core.like').toLowerCase()}s`; } else { reactionsLabelWrapper.innerHTML = ` ${I18n.t('core.like').toLowerCase()}`; } reactionsCountWrapper.id = 'reactions-count-' + publicReactionCounts[i].id; reactionsCountWrapper.innerHTML = publicReactionCounts[i].count; reactionsCountWrapper.classList.remove("hidden"); } else { reactionsCountWrapper.classList.add("hidden"); reactionsCountWrapper.innerHTML = '0'; } if (!buttForComment.classList.contains("reacted")) { buttForComment.setAttribute('aria-pressed', 'false') } } } for (var i = 0; i < allNodes.length; i++) { if (allNodes[i].dataset.commentAuthorId == responseObj.current_user.id) { allNodes[i].dataset.currentUserComment = "true"; var path = allNodes[i].dataset.path; var userActionsEl = allNodes[i].querySelector('.current-user-actions'); var buttEl = document.getElementById('button-for-comment-' + allNodes[i].dataset.commentId); if (userActionsEl && buttEl) { userActionsEl.innerHTML = `
  • Edit
  • Delete
  • ` userActionsEl.classList.remove('hidden'); } } } } }; ajaxReq.open("GET", "/reactions?commentable_id=" + commentableIdList[i] + "&commentable_type=" + commentableType, true); ajaxReq.send(); })(i); } })(); } var butts = document.getElementsByClassName('reaction-button'); for (var i = 0; i < butts.length; i++) { var butt = butts[i]; butt.onclick = function (event) { var thisButt = this; event.preventDefault(); sendHapticMessage('medium'); var userStatus = document.body.getAttribute('data-user-status'); if (userStatus === 'logged-out') { showLoginModal({ referring_source: 'comment', trigger: 'like' }); return; } thisButt.classList.add('reacted'); thisButt.disabled = true; function successCb(response) { var reactionCount = thisButt.querySelector('.reactions-count'); var reactionLabel = thisButt.querySelector('.reactions-label'); if (response.result === 'create') { thisButt.classList.add('reacted'); thisButt.setAttribute('aria-pressed', 'true'); if (reactionCount) { reactionCount.innerHTML = parseInt(reactionCount.innerHTML) + 1; reactionCount.classList.remove("hidden"); if(parseInt(reactionCount.innerHTML) == 1) { reactionLabel.innerHTML = " like" } else if(parseInt(reactionCount.innerHTML) > 1) { reactionLabel.innerHTML = " likes" } } } else { thisButt.classList.remove('reacted'); thisButt.setAttribute('aria-pressed', 'false'); if (reactionCount) { reactionCount.innerHTML = parseInt(reactionCount.innerHTML) - 1; if(parseInt(reactionCount.innerHTML) == 0) { reactionCount.classList.add("hidden"); thisButt.classList.remove('crayons-btn--icon-left'); thisButt.classList.add('crayons-btn--icon'); reactionLabel.innerHTML = "" } } } } var formData = new FormData(); formData.append('reactable_type', 'Comment'); formData.append('reactable_id', thisButt.dataset.commentId); getCsrfToken() .then(sendFetch('reaction-creation', formData)) .then(function (response) { thisButt.disabled = false; if (response.status === 200) { response.json().then(successCb); } else { showModalAfterError({ response, element: 'reaction', action_ing: 'making', action_past: 'made', }); } }); }; } var replyButts = document.getElementsByClassName('toggle-reply-form'); for (var i = 0; i < replyButts.length; i++) { var butt = replyButts[i]; butt.onclick = function (event) { event.preventDefault(); if (event.target.classList.contains("thread-indication")) { return false; } else { var userStatus = document.body.getAttribute('data-user-status'); if (userStatus == 'logged-out') { showLoginModal({ referring_source: 'comment', trigger: 'reply' }); return; } var parentId = event.target.closest('.comment').dataset.commentId; var waitingOnCSRF = setInterval(function () { var metaTag = document.querySelector("meta[name='csrf-token']"); if (metaTag) { clearInterval(waitingOnCSRF); commentWrapper = event.target.closest('.comment__details'); commentWrapper.classList.add("replying"); commentWrapper.innerHTML += buildCommentFormHTML(commentableId, commentableType, parentId); initializeCommentsPage(); setTimeout(function () { commentWrapper.getElementsByTagName('textarea')[0].focus(); }, 30); } }, 1); }; return false; } } if (document.getElementById('new_comment')) { document.getElementById('new_comment').addEventListener('submit', handleCommentSubmit); } } listenForDetailsToggle(); handleHiddenComments(commentableType); } function toggleCodeOfConduct() { var user = userData(); if (!user) { return; } var codeOfConduct = user.checked_code_of_conduct var checkboxWrapper = document.getElementById('toggle-code-of-conduct-checkbox'); if (checkboxWrapper && !codeOfConduct) { checkboxWrapper.innerHTML = '\ ' } } function handleCommentSubmit(event) { event.preventDefault(); var form = event.target; form.classList.add('submitting'); var textarea = form.getElementsByClassName('comment-textarea')[0]; if (textarea) { textarea.style.height = null; textarea.blur(); } var parentComment = document.getElementById("comment-node-" + event.target.dataset.commentId); // as there can be multiple forms rendered in the comments, we need to use querySelector to find descendants var commentParentId = form.querySelector("#comment_parent_id"); var body = JSON.stringify({ comment: { body_markdown: form.getElementsByTagName("textarea")[0].value, commentable_id: form.querySelector("#comment_commentable_id").value, commentable_type: form.querySelector("#comment_commentable_type").value, parent_id: commentParentId ? commentParentId.value : null, } }); getCsrfToken() .then(sendFetch('comment-creation', body)) .then(function (response) { if (response.status === 200) { response.json().then(function (newComment) { var newNode = document.createElement('div'); newNode.innerHTML = buildCommentHTML(newComment); var docBody = document.body var userData = JSON.parse(docBody.getAttribute('data-user')) userData.checked_code_of_conduct = true; docBody.dataset.user = JSON.stringify(userData); var checkbox = form.getElementsByClassName('code-of-conduct')[0] if (checkbox) { checkbox.innerHTML = "" } var mainCommentsForm = document.getElementById("new_comment"); if (parentComment) { handleFormClose(event); if (newComment.depth > 3) { var replyTrigger = parentComment.getElementsByClassName("toggle-reply-form")[0]; var threadIndicator = `${ iconSmallThread }Thread`; replyTrigger.classList.replace("inline-flex", "hidden"); replyTrigger.parentNode.innerHTML += threadIndicator; } var actionsNode = parentComment.getElementsByClassName("comment__inner")[0]; actionsNode.parentNode.insertBefore(newNode, actionsNode.nextSibling); } else if (mainCommentsForm) { var mainCommentsForm = document.getElementById("new_comment"); mainCommentsForm.classList.remove("submitting"); mainCommentsForm.classList.remove('preview-open'); const commentInputs = [...form.getElementsByClassName("comment-textarea")] commentInputs[0].closest('.comment-form').classList.remove('comment-form--initiated'); // Clearing out all comment textboxes and resetting their height because // there is an additional one generated by the comment // mention auto-complete component commentInputs.forEach(input => { input.value = newComment.comment_template || ""; input.style.height = null; }); const preview = document.getElementById("preview-div"); const togglePreview = document.querySelector('.preview-toggle'); preview.innerHTML = ""; togglePreview.innerHTML = "Preview"; const container = document.getElementById("comment-trees-container"); container.insertBefore(newNode, container.firstChild); } else if (document.getElementById("notifications-container")) { var newDiv = document.createElement("span") newDiv.innerHTML = '
    Reply sent — Check it out
    ' form.replaceWith(newDiv); } else { window.location.replace(newComment.url) } updateCommentsCount(); initializeCommentsPage(); initializeCommentDate(); activateRunkitTags(); }) } else { form.classList.remove('submitting'); showModalAfterError({ response, element: 'comment', action_ing: 'posting', action_past: 'posted', }); return false; } return false; }); return false; } function handleFocus(event) { handleButtonsActivation(event); var userStatus = document.body.getAttribute('data-user-status'); var area = event.target; if (userStatus == 'logged-out') { event.preventDefault(); showLoginModal({ referring_source: 'comments', trigger: 'new_comment'}); area.blur(); } else { var form = event.target.closest(".comment-form"); if (form) { form.classList.add("comment-form--initiated"); } handleSizeChange(event); window.Forem.initializeEnhancedCommentTextArea(area); } } function handleKeyUp(event) { handleSizeChange(event); handleButtonsActivation(event); } // Handler for when Ctrl+Enter OR Command+Enter is pressed function handleSubmit(event) { // Get user details and extract code of conduct & comment count var user = userData(); if (!user) { return; } var codeOfConduct = user.checked_code_of_conduct; if (codeOfConduct && event.target.value.trim() !== '') { event.target.closest('form').querySelector('button[type="submit"]').click(); } } var ENTER_KEY_CODE = 13; function handleKeyDown(event) { if (window.Forem.Runtime.hasOSSpecificModifier(event) && event.keyCode === ENTER_KEY_CODE) { event.preventDefault(); handleSubmit(event); } } function handleFormClose(event) { event.target.closest('.inner-comment').classList.remove("replying"); event.target.closest('.comment-form').remove(); initializeCommentsPage(); } function handleSizeChange(event) { var textarea = event.target; var oldHeight = parseInt(textarea.style.height.replace('px','')); textarea.style.height = textarea.scrollHeight + (textarea.scrollHeight > oldHeight ? 15 : 0) + "px"; } function handleButtonsActivation(event) { var textarea = event.target; var commentForm = textarea.closest('.comment-form'); if (commentForm) { var buttons = textarea.closest('.comment-form').getElementsByClassName('js-btn-enable'); Array.from(buttons).forEach(function(button) { button.disabled = textarea.value.length === 0; }); }; } function validateField(event) { // We only need to validate the textarea that is not the comment mention auto-complete component const textArea = event.target.form.querySelector('.comment-textarea:not([role=combobox])'); if (textArea.value === '') { event.preventDefault(); return; } } function handleChange(event) { handleButtonsActivation(event); } function generateUploadFormdata(image) { var token = document.querySelector("meta[name='csrf-token']").content; var formData = new FormData(); formData.append('authenticity_token', token); formData.append('image', image[0]); return formData; } function updateItemSummaryHtml(item) { var itemSummaryContent = item.getElementsByClassName("js-collapse-comment-content")[0]; var usernames = item.getElementsByClassName("js-comment-username"); var number = ""; if (usernames.length > 1) { number = " + " + (usernames.length - 1) + " replies" } var itemUsername = usernames[0].textContent + number if (item.open) { itemSummaryContent.textContent = ""; } else { itemSummaryContent.textContent = itemUsername; } } function listenForDetailsToggle() { var detailItems = document.querySelectorAll(".js-comment-wrapper"); for (var i = 0; i < detailItems.length; i++) { detailItems[i].addEventListener("toggle", event => { var item = event.target; updateItemSummaryHtml(item); }); } } /** * Increment comment, stored in `.js-comments-count`, count by one. */ function updateCommentsCount() { const commentsCountDiv = document.querySelector(".js-comments-count"); // if there's nowhere to put the count return early. if(!commentsCountDiv) return; const commentsCountData = parseInt(commentsCountDiv.dataset.commentsCount, 10) + 1; commentsCountDiv.dataset.commentsCount = commentsCountData; commentsCountDiv.innerHTML = `(${commentsCountData})` const commentsSidebarCountDiv = document.getElementById("reaction-number-comment"); if (!commentsSidebarCountDiv) return; commentsSidebarCountDiv.innerHTML = `${commentsCountData}`; } function initializeCommentDate() { const commentsDates = document.querySelectorAll('.comment-date time'); if (commentsDates) { addLocalizedDateTimeToElementsTitles(commentsDates, 'datetime'); } } function handleHiddenComments(commentableType){ const currentUser = userData(); const commentableAuthorIds = []; let coAuthorIds = ''; if(commentableType === "Article"){ const articleContainer = document.querySelector('#article-show-container'); if(articleContainer){ if (articleContainer.dataset) { commentableAuthorIds.push(articleContainer.dataset.authorId); coAuthorIds = articleContainer.dataset.coAuthorIds; if(coAuthorIds){ coAuthorIds.split(',').forEach(coAuthorId => { commentableAuthorIds.push(coAuthorId); }); } } } else { const commentsContainer = document.querySelector('#comments-container'); if(commentsContainer){ if(commentsContainer.dataset) { commentableAuthorIds.push(commentsContainer.dataset.commentableAuthorId); coAuthorIds = commentsContainer.dataset.commentableCoAuthorIds; if(coAuthorIds){ coAuthorIds.split(',').forEach(coAuthorId => { commentableAuthorIds.push(coAuthorId); }); } } } } } else if(commentableType === "PodcastEpisode"){ const podCastEpisodeContainer = document.querySelector('.podcast-episode-container'); if(podCastEpisodeContainer){ commentableAuthorIds.push(podCastEpisodeContainer.dataset.creatorId); } } if(currentUser && commentableAuthorIds.includes(currentUser.id.toString())){ collapseCommentsHiddenByCommentableUser(); } else { document.querySelectorAll('.element-hidden-by-commentable-user').forEach(element => { element.classList.add('hidden'); }) document.querySelectorAll('.comment-hidden-by-author-text').forEach(element => { element.classList.remove('hidden'); }) } } function collapseCommentsHiddenByCommentableUser() { document.querySelectorAll(".js-comment-wrapper.details-comment-hidden-by-commentable-user").forEach(item => { if (item.querySelectorAll('.comment-form').length === 0){ item.open = false; updateItemSummaryHtml(item); } }) }