From 7786be093e192ac7d8172a263d327d8ff4c50ebb Mon Sep 17 00:00:00 2001 From: Helen Chong <119173961+helenclx@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:08:49 +0800 Subject: [PATCH] Update guestbook control script --- src/_bundle/js/comments.js | 1069 ++++++++++++++++++------------------ 1 file changed, 527 insertions(+), 542 deletions(-) diff --git a/src/_bundle/js/comments.js b/src/_bundle/js/comments.js index f1ebd6e4..d6d28593 100644 --- a/src/_bundle/js/comments.js +++ b/src/_bundle/js/comments.js @@ -1,543 +1,528 @@ /** - * Author: Vera Konigin - * Site: https://groundedwren.neocities.org - * Contact: vera@groundedwren.com - * - * File Description: Comments Control -*/ - -/** - * By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element. - * If you have a lot of scripts, this can lead to clutter and naming collisions (what if two different scripts use a variable called "i"? They can inadvertently mess each other up). - * To get around this, we define the registerNamespace function in the global namespace, which just confines all the code in the function passed to it to a property under window. - * That property is represented as the "path" parameter. It is passed to the function for ease of access. -*/ -function registerNamespace(path, nsFunc) -{ - var ancestors = path.split("."); - - var ns = window; - for(var i = 0; i < ancestors.length; i++) - { - ns[ancestors[i]] = ns[ancestors[i]] || {}; - ns = ns[ancestors[i]]; - } - nsFunc(ns); -} - -registerNamespace("GW.Controls", function (ns) -{ - ns.CommentForm = class CommentForm extends HTMLElement - { - //#region staticProperties - static observedAttributes = []; - static instanceCount = 0; - static instanceMap = {}; - //#endregion - - //#region instance properties - instanceId; - isInitialized; - titleText; - discordURL; - encodedPath; - fallbackEmail; - - //#region element properties - formEl; - titleEl; - bannerEl; - - dispNameInpt; - emailInpt; - websiteInpt; - respToInpt; - commentInpt; - - resetBtn; - submitBtn; - //#endregion - //#endregion - - constructor() - { - super(); - this.instanceId = CommentForm.instanceCount++; - CommentForm.instanceMap[this.instanceId] = this; - } - - get idKey() - { - return `gw-comment-form-${this.instanceId}`; - } - - //#region HTMLElement implementation - connectedCallback() - { - if (this.isInitialized) { return; } - - this.titleText = this.getAttribute("titleText") || "Add a Comment"; - this.discordURL = this.getAttribute("discordURL"); - this.encodedPath = this.getAttribute("encodedPath"); - this.fallbackEmail = this.getAttribute("fallbackEmail"); - - this.renderContent(); - this.registerHandlers(); - - this.isInitialized = true; - } - //#endregion - - renderContent() - { - //Markup - this.innerHTML = ` -
-

${this.titleText}

-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- -

Comments are manually approved

-
- -
- `; - - //element properties - this.formEl = document.getElementById(`${this.idKey}-form`); - this.titleEl = document.getElementById(`${this.idKey}-title`); - this.bannerEl = document.getElementById(`${this.idKey}-banner`); - - this.dispNameInpt = document.getElementById(`${this.idKey}-dispName`); - this.emailInpt = document.getElementById(`${this.idKey}-email`); - this.websiteInpt = document.getElementById(`${this.idKey}-website`); - this.respToInpt = document.getElementById(`${this.idKey}-respTo`); - this.commentInpt = document.getElementById(`${this.idKey}-comment`); - - this.resetBtn = document.getElementById(`${this.idKey}-reset`); - this.submitBtn = document.getElementById(`${this.idKey}-submit`); - - //default values - this.dispNameInpt.value = localStorage.getItem("comment-name") || ""; - this.emailInpt.value = localStorage.getItem("comment-email") || ""; - this.websiteInpt.value = localStorage.getItem("comment-website") || ""; - } - - //#region Handlers - registerHandlers() - { - this.formEl.onsubmit = this.onSubmit; - } - - onSubmit = (event) => - { - event.preventDefault(); - - const contentObj = { - name: this.dispNameInpt.value, - email: this.emailInpt.value, - website: this.websiteInpt.value, - responseTo: this.respToInpt.value, - comment: this.commentInpt.value, - timestamp: new Date().toUTCString(), - }; - const contentAry = []; - for (let contentKey in contentObj) - { - contentAry.push(`${contentKey}=${contentObj[contentKey]}`); - } - - const request = new XMLHttpRequest(); - request.open( - "POST", - this.discordURL || atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3Mv" + this.encodedPath), - true - ); - request.setRequestHeader("Content-Type", "application/json"); - - request.onreadystatechange = () => - { - if (request.readyState !== XMLHttpRequest.DONE) { return; } - if (Math.floor(request.status / 100) !== 2) - { - console.log(request.responseText); - - this.bannerEl.classList.add("warning"); - this.bannerEl.innerHTML = - ` - - - That didn't work. - ${this.fallbackEmail - ? `Send your comment as an email instead.` - : "" - } - - `; - } - else - { - alert("Your comment has been submitted!"); - } - }; - - request.send(JSON.stringify({ content: contentAry.join("; ") })); - - localStorage.setItem("comment-name", contentObj.name); - localStorage.setItem("comment-email", contentObj.email); - localStorage.setItem("comment-website", contentObj.website); - - this.formEl.reset(); - this.dispNameInpt.value = contentObj.name; - this.emailInpt.value = contentObj.email; - this.websiteInpt.value = contentObj.website; - }; - //#endregion - }; - customElements.define("gw-comment-form", ns.CommentForm); - - ns.CommentList = class CommentList extends HTMLElement - { - //#region staticProperties - static observedAttributes = []; - static instanceCount = 0; - static instanceMap = {}; - //#endregion - - //#region instance properties - instanceId; - isInitialized; - gSpreadsheetId; - gSheetId; - isNewestFirst; - gwCommentFormId; - - //#region element properties - //#endregion - //#endregion - - constructor() - { - super(); - this.instanceId = CommentList.instanceCount++; - CommentList.instanceMap[this.instanceId] = this; - } - - get idKey() - { - return `gw-comment-list-${this.instanceId}`; - } - - //#region HTMLElement implementation - connectedCallback() - { - if (this.isInitialized) { return; } - - this.gSpreadsheetId = this.getAttribute("gSpreadsheetId"); - this.gSheetId = this.getAttribute("gSheetId"); - this.isNewestFirst = this.getAttribute("isNewestFirst"); - this.gwCommentFormId = this.getAttribute("gwCommentFormId"); - - this.loadAndRender(); - - this.isInitialized = true; - } - //#endregion - - async loadAndRender() - { - this.innerHTML = ` -
- - Comments loading.... -
- ` - - const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId); - const sheetData = await sheetReader.loadData(); - this.innerHTML = ""; - - const allComments = sheetReader.rowData; - if (this.isNewestFirst) - { - allComments.reverse(); - } - - this.renderContent(); - this.registerHandlers(); - - const allCommentsIndex = {}; - const topLevelCommentIdxs = []; - const childCommentIdxs = []; - for (let i = 0; i < allComments.length; i++) - { - const comment = allComments[i]; - allCommentsIndex[comment.ID] = i; - if (!comment.ResponseTo) - { - topLevelCommentIdxs.push(i); - } - else - { - childCommentIdxs.push(i); - } - } - childCommentIdxs.forEach(childIdx => - { - const replyId = allComments[childIdx].ResponseTo; - const respondeeComment = allComments[allCommentsIndex[replyId]]; - - respondeeComment.childrenIdxs = respondeeComment.childrenIdxs || []; - respondeeComment.childrenIdxs.push(childIdx); - }); - - let commentsToBuild = []; - topLevelCommentIdxs.forEach( - topCommentIdx => commentsToBuild.push( - { - parent: this.containerEl, - parentId: null, - comment: allComments[topCommentIdx] - } - ) - ); - - while (commentsToBuild.length > 0) - { - let { parent, parentId, comment } = commentsToBuild.shift(); - if (!comment.Timestamp) - { - continue; - } - parent.insertAdjacentHTML("beforeend", ` - - `); - - const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`); - (comment.childrenIdxs || []).forEach( - childIdx => commentsToBuild.push({ - parent: commentEl.articleEl, - parentId: comment.ID, - comment: allComments[childIdx] - }) - ); - } - } - - renderContent() - { - //Markup - this.innerHTML = ` -
-
- `; - - //element properties - this.containerEl = document.getElementById(`${this.idKey}-container`); - } - - //#region Handlers - registerHandlers() - { - } - //#endregion - }; - customElements.define("gw-comment-list", ns.CommentList); - - ns.CommentCard = class CommentCard extends HTMLElement - { - //#region staticProperties - static observedAttributes = []; - static instanceCount = 0; - static instanceMap = {}; - //#endregion - - //#region instance properties - instanceId; - isInitialized; - commentId; - replyToId; - numChildren; - commenterName; - isoTimestamp; - datetime; - websiteURL; - commentText; - gwCommentFormId; - - //#region element properties - articleEl; - replyBtn; - //#endregion - //#endregion - - constructor() - { - super(); - this.instanceId = CommentCard.instanceCount++; - CommentCard.instanceMap[this.instanceId] = this; - } - - get idKey() - { - return `gw-comment-card-${this.instanceId}`; - } - - //#region HTMLElement implementation - connectedCallback() - { - if (this.isInitialized) { return; } - - this.commentId = this.getAttribute("commentId"); - this.replyToId = this.getAttribute("replyToId"); - this.numChildren = this.getAttribute("numChildren"); - this.commenterName = this.getAttribute("commenterName"); - this.isoTimestamp = this.getAttribute("isoTimestamp"); - this.datetime = new Date(this.isoTimestamp); - this.websiteURL = this.getAttribute("websiteURL"); - this.commentText = this.getAttribute("commentText"); - this.gwCommentFormId = this.getAttribute("gwCommentFormId"); - - this.renderContent(); - this.registerHandlers(); - - this.isInitialized = true; - } - //#endregion - - renderContent() - { - let headerText = this.replyToId - ? `Comment #${this.commentId} replying to #${this.replyToId}` - : `Top level comment #${this.commentId}`; - - headerText += ` with ${this.numChildren} direct ${this.numChildren == 1 ? "reply" : "replies"}`; - - const displayTimestamp = this.datetime.toLocaleString( - undefined, - { dateStyle: "short", timeStyle: "short" } - ); - - const commenterNameEl = this.websiteURL - ? `${this.commenterName}` - : `${this.commenterName}`; - - //Markup - this.innerHTML = ` -
-
- - ${commenterNameEl} -
- - -
-
-
${this.commentText}
- -
- `; - - //element properties - this.articleEl = document.getElementById(`${this.idKey}-article`); - this.timestamp = document.getElementById(`${this.idKey}-timestamp`); - this.replyBtn = document.getElementById(`${this.idKey}-reply`); - this.hideBtn = document.getElementById(`${this.idKey}-hide`); - this.showBtn = document.getElementById(`${this.idKey}-show`); - } - - //#region Handlers - registerHandlers() - { - this.replyBtn.onclick = this.onReply; - this.hideBtn.onclick = this.onHide; - this.showBtn.onclick = this.onShow; - } - - onReply = () => - { - const gwCommentForm = document.getElementById(this.gwCommentFormId); - const respToInpt = gwCommentForm.respToInpt; - if (!respToInpt) - { - alert("Comment form not found"); - return; - } - - respToInpt.value = this.commentId; - respToInpt.focus(); - }; - - onHide = () => - { - this.classList.add("collapsed"); - this.showBtn.focus(); - }; - - onShow = () => - { - this.classList.remove("collapsed"); - this.timestamp.focus(); - }; - //#endregion - }; - customElements.define("gw-comment-card", ns.CommentCard); -}); \ No newline at end of file + * @file Comments control + * @author Vera Konigin vera@groundedwren.com + * https://groundedwren.neocities.org + */ + +window.GW = window.GW || {}; +(function Controls(ns) { + ns.CommentForm = class CommentForm extends HTMLElement { + //#region staticProperties + static instanceCount = 0; + static instanceMap = {}; + //#endregion + + //#region instance properties + instanceId; + isInitialized; + titleText; + discordURL; + encodedPath; + fallbackEmail; + + //#region element properties + formEl; + titleEl; + bannerEl; + + dispNameInpt; + emailInpt; + websiteInpt; + respToInpt; + commentInpt; + + resetBtn; + submitBtn; + //#endregion + //#endregion + + constructor() { + super(); + this.instanceId = CommentForm.instanceCount++; + CommentForm.instanceMap[this.instanceId] = this; + } + + get idKey() { + return `gw-comment-form-${this.instanceId}`; + } + + connectedCallback() { + if (this.isInitialized) { return; } + + this.titleText = this.getAttribute("titleText") || "Add a Comment"; + this.discordURL = this.getAttribute("discordURL"); + this.encodedPath = this.getAttribute("encodedPath"); + this.fallbackEmail = this.getAttribute("fallbackEmail"); + + this.renderContent(); + this.registerHandlers(); + + this.isInitialized = true; + } + + renderContent() { + //Markup + this.innerHTML = ` +
+

${this.titleText}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+ + Comments are manually approved +
+ +
+ `; + + //element properties + this.formEl = document.getElementById(`${this.idKey}-form`); + this.titleEl = document.getElementById(`${this.idKey}-title`); + this.bannerEl = document.getElementById(`${this.idKey}-banner`); + + this.dispNameInpt = document.getElementById(`${this.idKey}-dispName`); + this.emailInpt = document.getElementById(`${this.idKey}-email`); + this.websiteInpt = document.getElementById(`${this.idKey}-website`); + this.respToInpt = document.getElementById(`${this.idKey}-respTo`); + this.commentInpt = document.getElementById(`${this.idKey}-comment`); + + this.resetBtn = document.getElementById(`${this.idKey}-reset`); + this.submitBtn = document.getElementById(`${this.idKey}-submit`); + + //default values + this.dispNameInpt.value = localStorage.getItem("comment-name") || ""; + this.emailInpt.value = localStorage.getItem("comment-email") || ""; + this.websiteInpt.value = localStorage.getItem("comment-website") || ""; + } + + //#region Handlers + registerHandlers() { + this.formEl.onsubmit = this.onSubmit; + } + + onSubmit = (event) => { + event.preventDefault(); + + const contentObj = { + name: this.dispNameInpt.value, + email: this.emailInpt.value, + website: this.websiteInpt.value, + responseTo: this.respToInpt.value, + comment: ( + this.commentInpt.value || "" + ).replaceAll("\n", "
").replaceAll("(", "\\("), + timestamp: new Date().toUTCString(), + }; + const contentAry = []; + for (let contentKey in contentObj) { + contentAry.push(`${contentKey}=${contentObj[contentKey]}`); + } + + const request = new XMLHttpRequest(); + request.open( + "POST", + this.discordURL || atob("aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3Mv" + this.encodedPath), + true + ); + request.setRequestHeader("Content-Type", "application/json"); + + request.onreadystatechange = () => { + if (request.readyState !== XMLHttpRequest.DONE) { return; } + if (Math.floor(request.status / 100) !== 2) { + console.log(request.responseText); + this.bannerEl.classList.add("warning"); + this.bannerEl.innerHTML = + ` + + + That didn't work. + ${this.fallbackEmail + ? `Click here to send as an email instead.` + : "" + } + + `; + } + else { + alert("Your comment has been submitted!"); + } + }; + + request.send(JSON.stringify({ + embeds: [{ + fields: Object.keys(contentObj).map(key => { return { name: key, value: contentObj[key] }}) + }] + })); + + localStorage.setItem("comment-name", contentObj.name); + localStorage.setItem("comment-email", contentObj.email); + localStorage.setItem("comment-website", contentObj.website); + + this.formEl.reset(); + this.dispNameInpt.value = contentObj.name; + this.emailInpt.value = contentObj.email; + this.websiteInpt.value = contentObj.website; + }; + //#endregion + }; + customElements.define("gw-comment-form", ns.CommentForm); + + ns.CommentList = class CommentList extends HTMLElement { + //#region staticProperties + static instanceCount = 0; + static instanceMap = {}; + static Data = []; + //#endregion + + //#region instance properties + instanceId; + isInitialized; + gSpreadsheetId; + gSheetId; + isNewestFirst; + gwCommentFormId; + + //#region element properties + //#endregion + //#endregion + + constructor() { + super(); + this.instanceId = CommentList.instanceCount++; + CommentList.instanceMap[this.instanceId] = this; + CommentList.Data[this.instanceId] = {}; + } + + get idKey() { + return `gw-comment-list-${this.instanceId}`; + } + + connectedCallback() { + if (this.isInitialized) { return; } + + this.gSpreadsheetId = this.getAttribute("gSpreadsheetId"); + this.gSheetId = this.getAttribute("gSheetId"); + this.isNewestFirst = this.getAttribute("isNewestFirst"); + this.gwCommentFormId = this.getAttribute("gwCommentFormId"); + + this.loadAndRender(); + + this.isInitialized = true; + } + + async loadAndRender() { + this.innerHTML = ` +
+ + Comments loading.... +
+ ` + + const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId); + await sheetReader.loadData(); + this.innerHTML = ""; + + const allComments = sheetReader.rowData; + if (this.isNewestFirst) { + allComments.reverse(); + } + + this.renderContent(); + this.registerHandlers(); + + const allCommentsIndex = {}; + const topLevelCommentIdxs = []; + const childCommentIdxs = []; + for (let i = 0; i < allComments.length; i++) { + const comment = allComments[i]; + allCommentsIndex[comment.ID] = i; + if (!comment.ResponseTo) { + topLevelCommentIdxs.push(i); + } + else { + childCommentIdxs.push(i); + } + } + childCommentIdxs.forEach(childIdx => { + const replyId = allComments[childIdx].ResponseTo; + const respondeeComment = allComments[allCommentsIndex[replyId]]; + + respondeeComment.ChildIdxs = respondeeComment.ChildIdxs || []; + respondeeComment.ChildIdxs.push(childIdx); + }); + + let commentsToBuild = []; + topLevelCommentIdxs.forEach( + topCommentIdx => commentsToBuild.push({ + parent: this.containerEl, + comment: allComments[topCommentIdx] + }) + ); + + while (commentsToBuild.length > 0) { + let { parent, comment } = commentsToBuild.shift(); + if (!comment.Timestamp) { + continue; + } + + CommentList.Data[this.instanceId][comment.ID] = comment; + + parent.insertAdjacentHTML("beforeend", ` + + `); + + const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`); + (comment.ChildIdxs || []).forEach( + childIdx => commentsToBuild.push({ + parent: commentEl.articleEl, + comment: allComments[childIdx] + }) + ); + } + } + + renderContent() { + //Markup + this.innerHTML = ` +
+
+ `; + + //element properties + this.containerEl = document.getElementById(`${this.idKey}-container`); + } + + //#region Handlers + registerHandlers() { + } + //#endregion + }; + customElements.define("gw-comment-list", ns.CommentList); + + ns.CommentCard = class CommentCard extends HTMLElement { + //#region staticProperties + static instanceCount = 0; + static instanceMap = {}; + //#endregion + + //#region instance properties + instanceId; + isInitialized; + + commentId; + gwCommentFormId; + + replyToId; + numChildren; + commenterName; + datetime; + websiteURL; + commentText; + + //#region element properties + articleEl; + replyBtn; + //#endregion + //#endregion + + constructor() { + super(); + this.instanceId = CommentCard.instanceCount++; + CommentCard.instanceMap[this.instanceId] = this; + } + + get idKey() { + return `gw-comment-card-${this.instanceId}`; + } + + //#region HTMLElement implementation + connectedCallback() { + if (this.isInitialized) { return; } + + this.commentId = this.getAttribute("commentId"); + this.gwCommentFormId = this.getAttribute("gwCommentFormId"); + + const commentData = ns.CommentList.Data[this.getAttribute("listInstance")][this.commentId]; + + this.replyToId = commentData.ResponseTo; + this.numChildren = (commentData.ChildIdxs || []).length; + this.commenterName = commentData["Display Name"]; + this.datetime = commentData.Timestamp; + this.websiteURL = commentData.Website; + this.commentText = this.parseCommentText(commentData.Comment); + + this.renderContent(); + this.registerHandlers(); + + this.isInitialized = true; + } + //#endregion + + renderContent() { + let headerText = this.replyToId + ? `Comment #${this.commentId} replying to #${this.replyToId}` + : `Top level comment #${this.commentId}`; + headerText += ` with ${this.numChildren} direct ${this.numChildren == 1 ? "reply" : "replies"}`; + + const displayTimestamp = this.datetime.toLocaleString( + undefined, + { dateStyle: "short", timeStyle: "short" } + ); + + const commenterNameEl = this.websiteURL + ? `${this.commenterName}` + : `${this.commenterName}`; + + //Markup + this.innerHTML = ` +
+
+ + ${commenterNameEl} +
+ + +
+
+
${this.commentText}
+ +
+ `; + + //element properties + this.articleEl = document.getElementById(`${this.idKey}-article`); + this.timestamp = document.getElementById(`${this.idKey}-timestamp`); + this.replyBtn = document.getElementById(`${this.idKey}-reply`); + this.hideBtn = document.getElementById(`${this.idKey}-hide`); + this.showBtn = document.getElementById(`${this.idKey}-show`); + } + + //#region Handlers + registerHandlers() { + this.replyBtn.onclick = this.onReply; + this.hideBtn.onclick = this.onHide; + this.showBtn.onclick = this.onShow; + } + + onReply = () => { + const gwCommentForm = document.getElementById(this.gwCommentFormId); + const respToInpt = gwCommentForm.respToInpt; + if (!respToInpt) { + alert("Comment form not found"); + return; + } + + respToInpt.value = this.commentId; + respToInpt.focus(); + }; + + onHide = () => { + this.classList.add("collapsed"); + this.showBtn.focus(); + }; + + onShow = () => { + this.classList.remove("collapsed"); + this.timestamp.focus(); + }; + //#endregion + + parseCommentText(commentString) { + let commentText = ""; + let linkObj = {}; + + for(let i = 0; i < commentString.length; i++){ + let char = commentString.charAt(i); + switch (char) { + case '[': + linkObj = {tStart: i}; + break; + case ']': + if(linkObj.tStart !== undefined && linkObj.tStart !== i-1) { + linkObj.tEnd = i; + } + else { linkObj = {}; } + break; + case '(': + if(linkObj.tEnd !== undefined && linkObj.tEnd === i-1) { + linkObj.lStart = i; + } + else { linkObj = {}; } + break; + case ')': + if(linkObj.lStart !== undefined && linkObj.lStart !== i-1) { + linkObj.lEnd = i; + } + else { linkObj = {}; } + break; + } + if(linkObj.lEnd !== undefined) { + const linkText = commentString.substring(linkObj.tStart + 1, linkObj.tEnd); + const linkURL = commentString.substring(linkObj.lStart + 1, linkObj.lEnd); + commentText = commentText.substring(0, commentText.length - (i - linkObj.tStart)); + commentText += `${linkText}`; + linkObj = {}; + } + else { + commentText += char; + } + } + return commentText; + } + }; + customElements.define("gw-comment-card", ns.CommentCard); +}) (window.GW.Controls = window.GW.Controls || {}); \ No newline at end of file