/** * Author: Vera Konigin * Site: https://groundedwren.neocities.org * Contact: vera@groundedwren.com * * File Description: Comments Control */ 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; //#region element properties formEl; titleEl; 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.renderContent(); this.registerHandlers(); this.isInitialized = true; } //#endregion renderContent() { //Markup this.innerHTML = ` <form id="${this.idKey}-form" aria-labelledby="${this.idKey}-title" aria-describedby="${this.idKey}-banner" class="comment-form" autocomplete="off" > <h2 id="${this.idKey}-title" class="comment-form-title">${this.titleText}</h2> <div class="input-horizontal-wrapper"> <div class="input-vertical"> <label for="${this.idKey}-dispName"> Display name<span aria-hidden="true">*</span> <span class="visually-hidden">(Required)</span> </label> <input id="${this.idKey}-dispName" type="text" maxlength="1000" required="true"> </div> <div class="input-vertical"> <label for="${this.idKey}-email">Email</label> <input id="${this.idKey}-email" type="email"> </div> <div class="input-vertical"> <label for="${this.idKey}-website">Website</label> <input id="${this.idKey}-website" type="text" maxlength="1000"> </div> <div class="input-vertical"> <label for="${this.idKey}-respTo">Response to</label> <input id="${this.idKey}-respTo" type="number"> </div> </div> <div class="comment-box-container"> <label for="${this.idKey}-comment"> Comment<span aria-hidden="true">*</span> <span class="visually-hidden">(Required)</span> </label> <textarea id="${this.idKey}-comment" minlength="1" maxlength="4000" required="true" rows="5" ></textarea> </div> <div id="${this.idKey}-banner" class="inline-banner"> <gw-icon iconKey="circle-info" alt="info"></gw-icon> <p>Comments are manually approved</p> </div> <div class="form-footer"> <input id="${this.idKey}-reset" type="reset" value="Reset"> <input id="${this.idKey}-submit" type="submit" value="Submit"> </div> </form> `; //element properties this.formEl = document.getElementById(`${this.idKey}-form`); this.titleEl = document.getElementById(`${this.idKey}-title`); 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, true ); request.setRequestHeader("Content-Type", "application/json"); request.onreadystatechange = function () { if (request.readyState == 4) { console.log(request.responseText); } }; 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; alert("Your comment has been submitted!"); }; //#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 = ` <div class="inline-banner"> <gw-icon iconkey="circle-info" title="info"></gw-icon> <span>Comments loading....</span> </div> ` 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", ` <gw-comment-card id="${this.idKey}-cmt-${comment.ID}" commentId="${comment.ID || ""}" replyToId="${parentId || ""}" commenterName="${comment["Display Name"] || ""}" isoTimestamp="${comment.Timestamp.toISOString()}" websiteURL="${comment.Website || ""}" commentText="${comment.Comment || ""}" gwCommentFormId="${this.gwCommentFormId || ""}" ></gw-comment-card> `); 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 = ` <div id="${this.idKey}-container" class="comments-container""> </div> `; //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; 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.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() { const headerText = this.replyToId ? `Comment #${this.commentId} replying to #${this.replyToId}` : `Top level comment #${this.commentId}`; const displayTimestamp = this.datetime.toLocaleString( undefined, { dateStyle: "short", timeStyle: "short" } ); const commenterNameEl = this.websiteURL ? `<a href="${this.websiteURL}" target="_blank" class="commenter-name">${this.commenterName}</a>` : `<span class="commenter-name">${this.commenterName}</span>`; //Markup this.innerHTML = ` <article id="${this.idKey}-article" aria-labelledby="${this.idKey}-header" class="comment-article" > <div id="${this.idKey}-header" class="comment-header"> <div> <span aria-hidden="true" class="comment-id">#${this.commentId}</span> <span class="visually-hidden">${headerText}</span> </div> ${commenterNameEl} <time datetime="${this.isoTimestamp}" class="comment-timestamp">${displayTimestamp}</time> </div> <blockquote>${this.commentText}</blockquote> <button id="${this.idKey}-reply">Reply to #${this.commentId}</button> </article> `; //element properties this.articleEl = document.getElementById(`${this.idKey}-article`); this.replyBtn = document.getElementById(`${this.idKey}-reply`); } //#region Handlers registerHandlers() { this.replyBtn.onclick = this.onReply; } 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(); }; //#endregion }; customElements.define("gw-comment-card", ns.CommentCard); });