/* (PLEASE DO NOT DELETE THIS HEADER OR CREDIT!) User customizable settings below! Please refer to my guide over on https://virtualobserver.moe/ayano/comment-widget if you're confused on how to use this. The IDs at the top are a requirement but everything else is optional! Do not delete any settings even if you aren't using them! It could break the program. After filling out your options, just paste this anywhere you want a comment section (But change the script src URL to wherever you have this widget stored on your site!)
Have fun! Bug reports are encouraged if you happen to run into any issues. - Ayano (https://virtualobserver.moe/) */ // The values in this section are REQUIRED for the widget to work! Keep them in quotes! const s_stylePath = 'comment.css'; const s_formId = '1FAIpQLSd8770fPg39fifCiy2KIuOj55OwZJAAbm5S1_2dynDFjbLE2A'; const s_nameId = '468380077'; const s_websiteId = '687771565'; const s_textId = '2032170559'; const s_pageId = '880771942'; const s_replyId = '204637977'; const s_sheetId = 'https://docs.google.com/spreadsheets/d/1_oOWScQwaPwGlo5QVD9Qb5aZh7b1QvD2j1I9gSoyU2I/edit?resourcekey#gid=1304949159'; // The values below are necessary for accurate timestamps, I've filled it in with EST as an example const s_timezone = -5; // Your personal timezone (Example: UTC-5:00 is -5 here, UTC+10:30 would be 10.5) const s_daylightSavings = true; // If your personal timezone uses DST, set this to true // For the dates DST start and end where you live: [Month, Weekday, which number of that weekday, hour (24 hour time)] const s_dstStart = ['March', 'Sunday', 2, 2]; // Example shown is the second Sunday of March at 2:00 am const s_dstEnd = ['November', 'Sunday', 1, 2]; // Example shown is the first Sunday of November at 2:00 am // Misc - Other random settings const s_commentsPerPage = 5; // The max amount of comments that can be displayed on one page, any number >= 1 (Replies not counted) const s_maxLength = 500; // The max character length of a comment const s_maxLengthName = 16; // The max character length of a name const s_commentsOpen = true; // Change to false if you'd like to close your comment section site-wide (Turn it off on Google Forms too!) const s_collapsedReplies = true; // True for collapsed replies with a button, false for replies to display automatically const s_longTimestamp = false; // True for a date + time, false for just the date let s_includeUrlParameters = false; // Makes new comment sections on pages with URL parameters when set to true (If you don't know what this does, leave it disabled) const s_fixRarebitIndexPage = false; // If using Rarebit, change to true to make the index page and page 1 of your webcomic have the same comment section // Word filter - Censor profanity, etc const s_wordFilterOn = false; // True for on, false for off const s_filterReplacement = '****'; // Change what filtered words are censored with (**** is the default) const s_filteredWords = [ // Add words to filter by putting them in quotes and separating with commas (ie. 'heck', 'dang') 'heck', 'dang' ] // Text - Change what messages/text appear on the form and in the comments section (Mostly self explanatory) const s_widgetTitle = 'Leave a comment!'; const s_nameFieldLabel = 'Name'; const s_websiteFieldLabel = 'Website (Optional)'; const s_textFieldLabel = ''; const s_submitButtonLabel = 'Submit'; const s_loadingText = 'Loading comments...'; const s_noCommentsText = 'No comments yet!'; const s_closedCommentsText = 'Comments are closed temporarily!'; const s_websiteText = 'Website'; // The links to websites left by users on their comments const s_replyButtonText = 'Reply'; // The button for replying to someone const s_replyingText = 'Replying to'; // The text that displays while the user is typing a reply const s_expandRepliesText = 'Show Replies'; const s_leftButtonText = '<<'; const s_rightButtonText = '>>'; /* DO NOT edit below this point unless you are confident you know what you're doing! Everything else is automatic, you don't have to change anything else. ^^ However, feel free to edit this code as much as you like! Just please don't remove my credit if possible <3 */ // Fix the URL parameters setting for Rarebit just in case if (s_fixRarebitIndexPage) {s_includeUrlParameters = true} // Apply CSS const c_cssLink = document.createElement('link'); c_cssLink.type = 'text/css'; c_cssLink.rel = 'stylesheet'; c_cssLink.href = s_stylePath; document.getElementsByTagName('head')[0].appendChild(c_cssLink); // HTML Form const v_mainHtml = `
${s_loadingText}
`; const v_formHtml = `

${s_widgetTitle}

`; // Insert main HTML to page document.getElementById('c_widget').innerHTML = v_mainHtml; const c_form = document.getElementById('c_form'); if (s_commentsOpen) {c_form.innerHTML = v_formHtml} else {c_form.innerHTML = s_closedCommentsText} // Initialize misc things const c_container = document.getElementById('c_container'); let v_pageNum = 1; let v_amountOfPages = 1; let v_commentMax = 1; let v_commentMin = 1; // Set up the word filter if applicable let v_filteredWords; if (s_wordFilterOn) { v_filteredWords = s_filteredWords.join('|'); v_filteredWords = new RegExp(String.raw `\b(${v_filteredWords})\b`, 'ig'); } // The fake button is just a dummy placeholder for when comments are closed let c_submitButton; if (s_commentsOpen) {c_submitButton = document.getElementById('c_submitButton')} else {c_submitButton = document.createElement('button')} // Add invisible page input to document let v_pagePath = window.location.pathname; if (s_includeUrlParameters) {v_pagePath += window.location.search} if (s_fixRarebitIndexPage && v_pagePath == '/') {v_pagePath = '/?pg=1'} const c_pageInput = document.createElement('input'); c_pageInput.value = v_pagePath; c_pageInput.type = 'text'; c_pageInput.style.display = 'none'; c_pageInput.id = 'entry.' + s_pageId; c_pageInput.name = c_pageInput.id; c_form.appendChild(c_pageInput); // Add the "Replying to..." text to document let c_replyingText = document.createElement('span'); c_replyingText.style.display = 'none'; c_replyingText.id = 'c_replyingText'; c_form.appendChild(c_replyingText); c_replyingText = document.getElementById('c_replyingText'); // Add the invisible reply input to document let c_replyInput = document.createElement('input'); c_replyInput.type = 'text'; c_replyInput.style.display = 'none'; c_replyInput.id = 'entry.' + s_replyId; c_replyInput.name = c_replyInput.id; c_form.appendChild(c_replyInput); c_replyInput = document.getElementById('entry.' + s_replyId); // Add the invisible iFrame to the document for catching the default Google Forms submisson page let v_submitted = false; let c_hiddenIframe = document.createElement('iframe'); c_hiddenIframe.id = 'c_hiddenIframe'; c_hiddenIframe.name = 'c_hiddenIframe'; c_hiddenIframe.style.display = 'none'; c_hiddenIframe.setAttribute('onload', 'if(v_submitted){fixFrame()}'); c_form.appendChild(c_hiddenIframe); c_hiddenIframe = document.getElementById('c_hiddenIframe'); // Fix the invisible iFrame so it doesn't keep trying to load stuff function fixFrame() { v_submitted = false; c_hiddenIframe.srcdoc = ''; getComments(); // Reload comments after submission } // Processes comment data with the Google Sheet ID function getComments() { // Disable the submit button while comments are reloaded c_submitButton.disabled; // Reset reply stuff to default c_replyingText.style.display = 'none'; c_replyInput.value = ''; // Clear input fields too document.getElementById(`entry.${s_nameId}`).value = ''; document.getElementById(`entry.${s_websiteId}`).value = ''; document.getElementById(`entry.${s_textId}`).value = ''; // Get the data const url = `https://docs.google.com/spreadsheets/d/${s_sheetId}/gviz/tq?`; const retrievedSheet = getSheet(url); // Do stuff with the data here retrievedSheet.then(result => { // The data comes with extra stuff at the beginning, get rid of it const json = JSON.parse(result.split('\n')[1].replace(/google.visualization.Query.setResponse\(|\);/g, '')); // Need index of page column for checking if comments are for the right page const isPage = (col) => col.label == 'Page'; let pageIdx = json.table.cols.findIndex(isPage); // Turn that data into usable comment data // All of the messy val checks are because Google Sheets can be weird sometimes with comment deletion let comments = []; if (json.table.parsedNumHeaders > 0) { // Check if any comments exist in the sheet at all before continuing for (r = 0; r < json.table.rows.length; r++) { // Check for null rows let val1; if (!json.table.rows[r].c[pageIdx]) {val1 = ''} else {val1 = json.table.rows[r].c[pageIdx].v} // Check if the page name matches before adding to comment array if (val1 == v_pagePath) { let comment = {} for (c = 0; c < json.table.cols.length; c++) { // Check for null values let val2; if (!json.table.rows[r].c[c]) {val2 = ''} else {val2 = json.table.rows[r].c[c].v} // Finally set the value properly comment[json.table.cols[c].label] = val2; } comment.Timestamp2 = json.table.rows[r].c[0].f; comments.push(comment); } } } // Check for empty comments before displaying to page if (comments.length == 0 || Object.keys(comments[0]).length < 2) { // Once again, Google Sheets can be weird c_container.innerHTML = s_noCommentsText; } else {displayComments(comments)} c_submitButton.disabled = false // Now that everything is done, re-enable the submit button }) } // Fetches the Google Sheet resource from the provided URL function getSheet(url) { return new Promise(function (resolve, reject) { fetch(url).then(response => { if (!response.ok) {reject('Could not find Google Sheet with that URL')} // Checking for a 404 else { response.text().then(data => { if (!data) {reject('Invalid data pulled from sheet')} resolve(data); }) } }) }) } // Displays comments on page let a_commentDivs = []; // For use in other functions function displayComments(comments) { // Clear for re-display a_commentDivs = []; c_container.innerHTML = ''; // Get all reply comments by taking them out of the comment array let replies = []; for (i = 0; i < comments.length; i++) { if (comments[i].Reply) { replies.push(comments[i]); comments.splice(i, 1); i--; } } // Values for pagination v_amountOfPages = Math.ceil(comments.length / s_commentsPerPage); v_commentMax = s_commentsPerPage * v_pageNum; v_commentMin = v_commentMax - s_commentsPerPage; // Main comments (not replies) comments.reverse(); // Newest comments go to top for (i = 0; i < comments.length; i++) { let comment = createComment(comments[i]); // Reply button let button = document.createElement('button'); button.innerHTML = s_replyButtonText; button.value = comment.id; button.setAttribute('onclick', `openReply(this.value)`); button.className = 'c-replyButton'; comment.appendChild(button); // Choose whether to display or not based on page number comment.style.display = 'none'; if (i >= v_commentMin && i < v_commentMax) {comment.style.display = 'block'} comment.className = 'c-comment'; c_container.appendChild(comment); a_commentDivs.push(document.getElementById(comment.id)); // Add to array for use later } // Replies for (i = 0; i < replies.length; i++) { let reply = createComment(replies[i]); const parentId = replies[i].Reply; const parentDiv = document.getElementById(parentId); // Check if a container doesn't already exist for this comment, if not, make one let container; if (!document.getElementById(parentId + '-replies')) { container = document.createElement('div'); container.id = parentId + '-replies'; if (s_collapsedReplies) {container.style.display = 'none'} // Default to hidden if collapsed container.className = 'c-replyContainer'; parentDiv.appendChild(container); } else {container = document.getElementById(parentId + '-replies')} reply.className = 'c-reply'; container.appendChild(reply); } // Handle adding the buttons to show or hide replies if collapsed replies are enabled if (s_collapsedReplies) { const containers = document.getElementsByClassName('c-replyContainer'); for (i = 0; i < containers.length; i++) { const num = containers[i].childNodes.length; const parentDiv = containers[i].parentElement; // The button to expand replies const button = document.createElement('button'); button.innerHTML = s_expandRepliesText + ` (${num})`; button.setAttribute('onclick', `expandReplies(this.parentElement.id)`); button.className = 'c-expandButton'; parentDiv.insertBefore(button, parentDiv.lastChild); } } // Handle pagination if there's more than one page if (v_amountOfPages > 1) { let pagination = document.createElement('div'); leftButton = document.createElement('button'); leftButton.innerHTML = s_leftButtonText; leftButton.id = 'c_leftButton'; leftButton.name = 'left'; leftButton.setAttribute('onclick', `changePage(this.name)`); if (v_pageNum == 1) {leftButton.disabled = true} // Can't go before page 1 leftButton.className = 'c-paginationButton'; pagination.appendChild(leftButton); rightButton = document.createElement('button'); rightButton.innerHTML = s_rightButtonText; rightButton.id = 'c_rightButton'; rightButton.name = 'right'; rightButton.setAttribute('onclick', `changePage(this.name)`); if (v_pageNum == v_amountOfPages) {rightButton.disabled = true} // Can't go after the last page rightButton.className = 'c-paginationButton'; pagination.appendChild(rightButton); pagination.id = 'c_pagination'; c_container.appendChild(pagination); } } // Create basic HTML comment, reply or not function createComment(data) { let comment = document.createElement('div'); // Get the right timestamps let timestamps = convertTimestamp(data.Timestamp); let timestamp; if (s_longTimestamp) {timestamp = timestamps[0]} else {timestamp = timestamps[1]} // Set the ID (uses Name + Full Timestamp format) const id = data.Name + '|--|' + data.Timestamp2; comment.id = id; // Name of user let name = document.createElement('h3'); let filteredName = data.Name; if (s_wordFilterOn) {filteredName = filteredName.replace(v_filteredWords, s_filterReplacement)} name.innerText = filteredName; name.className = 'c-name'; comment.appendChild(name); // Timestamp let time = document.createElement('span'); time.innerText = timestamp; time.className = 'c-timestamp'; comment.appendChild(time); // Website URL, if one was provided if (data.Website) { let site = document.createElement('a'); site.innerText = s_websiteText; site.href = data.Website; site.className = 'c-site'; comment.appendChild(site); } // Text content let text = document.createElement('p'); let filteredText = data.Text; if (s_wordFilterOn) {filteredText = filteredText.replace(v_filteredWords, s_filterReplacement)} text.innerText = filteredText; text.className = 'c-text'; comment.appendChild(text); return comment; } // Makes the Google Sheet timestamp usable function convertTimestamp(timestamp) { const vals = timestamp.split('(')[1].split(')')[0].split(','); const date = new Date(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]); const timezoneDiff = (s_timezone * 60 + date.getTimezoneOffset()) * -1; let offsetDate = new Date(date.getTime() + timezoneDiff * 60 * 1000); if (s_daylightSavings) {offsetDate = isDST(offsetDate)} return [offsetDate.toLocaleString(), offsetDate.toLocaleDateString()]; } // DST checker function isDST(date) { const dstStart = [getMonthNum(s_dstStart[0]), getDayNum(s_dstStart[1]), s_dstStart[2], s_dstStart[3]]; const dstEnd = [getMonthNum(s_dstEnd[0]), getDayNum(s_dstEnd[1]), s_dstEnd[2], s_dstEnd[3]]; const year = date.getFullYear(); let startDate = new Date(year, dstStart[0], 1); startDate = nthDayOfMonth(dstStart[1], dstStart[2], startDate, dstStart[3]).getTime(); let endDate = new Date(year, dstEnd[0], 1); endDate = nthDayOfMonth(dstEnd[1], dstEnd[2], endDate, dstEnd[3]).getTime(); time = date.getTime(); if (time >= startDate && time < endDate) {date.setHours(date.getHours() - 1)} return date; } // Thank you to https://stackoverflow.com/questions/32192982/get-a-given-weekday-in-a-given-month-with-javascript for the below function function nthDayOfMonth(day, n, date, hour) { var count = 0; var idate = new Date(date); idate.setDate(1); while ((count) < n) { idate.setDate(idate.getDate() + 1); if (idate.getDay() == day) { count++; } } idate.setHours(hour); return idate; } // Convert weekday and month names into numbers function getDayNum(day) { let num; switch (day.toLowerCase()) { case 'sunday': num = 0; break; case 'monday': num = 1; break; case 'tuesday': num = 2; break; case 'wednesday': num = 3; break; case 'thursday': num = 4; break; case 'friday': num = 5; break; case 'saturday': num = 6; break; default: num = 0; break; } return num; } function getMonthNum(month) { let num; switch (month.toLowerCase()) { case 'january': num = 0; break; case 'february': num = 1; break; case 'march': num = 2; break; case 'april': num = 3; break; case 'may': num = 4; break; case 'june': num = 5; break; case 'july': num = 6; break; case 'august': num = 7; break; case 'september': num = 8; break; case 'october': num = 9; break; case 'november': num = 10; break; case 'december': num = 11; break; } return num; } // Handle making replies const link = document.createElement('a'); link.href = '#c_inputDiv'; function openReply(id) { if (c_replyingText.style.display == 'none') { c_replyingText.innerHTML = s_replyingText + ` ${id.split('|--|')[0]}...`; c_replyInput.value = id; c_replyingText.style.display = 'block'; } else { c_replyingText.innerHTML = ''; c_replyInput.value = ''; c_replyingText.style.display = 'none'; } link.click(); // Jump to the space to type } // Handle expanding replies (should only be accessible with collapsed replies enabled) function expandReplies(id) { const targetDiv = document.getElementById(`${id}-replies`); if (targetDiv.style.display == 'none') {targetDiv.style.display = 'block'} else {targetDiv.style.display = 'none'} } function changePage(dir) { const leftButton = document.getElementById('c_leftButton'); const rightButton = document.getElementById('c_rightButton'); // Find directional number let num; switch (dir) { case 'left': num = -1; break; case 'right': num = 1; break; default: num = 0; break; } let targetPage = v_pageNum + num; // Cancel if impossible direction for safety, should never happen though if (targetPage > v_amountOfPages || targetPage < 1) {return} // Enable/disable buttons if needed leftButton.disabled = false; rightButton.disabled = false; if (targetPage == 1) {leftButton.disabled = true} // Can't go before page 1 if (targetPage == v_amountOfPages) {rightButton.disabled = true} // Can't go past the last page // Hide all comments and then display the correct ones v_pageNum = targetPage; v_commentMax = s_commentsPerPage * v_pageNum; v_commentMin = v_commentMax - s_commentsPerPage; for (i = 0; i < a_commentDivs.length; i++) { a_commentDivs[i].style.display = 'none'; if (i >= v_commentMin && i < v_commentMax) {a_commentDivs[i].style.display = 'block'} } } getComments(); // Run once on page load