Compare commits

..

3 Commits

Author SHA1 Message Date
Helen Chong e051095ce3 Add note about guestbook comment field supporting HTML 2024-07-24 13:20:46 +08:00
Helen Chong f4410db8b3 Fix overflow on mobile due to long name of a commentor 2024-07-24 13:10:13 +08:00
Helen Chong 7786be093e Update guestbook control script 2024-07-24 13:08:49 +08:00
3 changed files with 530 additions and 544 deletions

View File

@ -110,7 +110,7 @@ gw-comment-card.collapsed .comment-header-right time { display: none; }
.comment-header { .comment-header {
display: grid; display: grid;
grid-template-columns: 0fr max-content 1fr; grid-template-columns: 0fr auto 1fr;
gap: 0.4em; gap: 0.4em;
align-items: baseline; align-items: baseline;
} }
@ -136,4 +136,5 @@ gw-comment-card.collapsed .comment-header-right time { display: none; }
.commenter-name { .commenter-name {
font-size: 1.1em; font-size: 1.1em;
font-weight: 700; font-weight: 700;
overflow-wrap: break-word;
} }

View File

@ -1,36 +1,13 @@
/** /**
* Author: Vera Konigin * @file Comments control
* Site: https://groundedwren.neocities.org * @author Vera Konigin vera@groundedwren.com
* Contact: vera@groundedwren.com * https://groundedwren.neocities.org
*
* File Description: Comments Control
*/ */
/** window.GW = window.GW || {};
* By default, any JavaScript code written is defined in the global namespace, which means it's accessible directly under the "window" element. (function Controls(ns) {
* 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). ns.CommentForm = class CommentForm extends HTMLElement {
* 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 //#region staticProperties
static observedAttributes = [];
static instanceCount = 0; static instanceCount = 0;
static instanceMap = {}; static instanceMap = {};
//#endregion //#endregion
@ -59,21 +36,17 @@ registerNamespace("GW.Controls", function (ns)
//#endregion //#endregion
//#endregion //#endregion
constructor() constructor() {
{
super(); super();
this.instanceId = CommentForm.instanceCount++; this.instanceId = CommentForm.instanceCount++;
CommentForm.instanceMap[this.instanceId] = this; CommentForm.instanceMap[this.instanceId] = this;
} }
get idKey() get idKey() {
{
return `gw-comment-form-${this.instanceId}`; return `gw-comment-form-${this.instanceId}`;
} }
//#region HTMLElement implementation connectedCallback() {
connectedCallback()
{
if (this.isInitialized) { return; } if (this.isInitialized) { return; }
this.titleText = this.getAttribute("titleText") || "Add a Comment"; this.titleText = this.getAttribute("titleText") || "Add a Comment";
@ -86,10 +59,8 @@ registerNamespace("GW.Controls", function (ns)
this.isInitialized = true; this.isInitialized = true;
} }
//#endregion
renderContent() renderContent() {
{
//Markup //Markup
this.innerHTML = ` this.innerHTML = `
<form id="${this.idKey}-form" <form id="${this.idKey}-form"
@ -120,19 +91,21 @@ registerNamespace("GW.Controls", function (ns)
</div> </div>
</div> </div>
<div class="comment-box-container"> <div class="comment-box-container">
<div class="input-vertical">
<label for="${this.idKey}-comment"> <label for="${this.idKey}-comment">
Comment<span aria-hidden="true">*</span> Comment<span aria-hidden="true">*</span>
</label> </label>
<textarea id="${this.idKey}-comment" <textarea id="${this.idKey}-comment"
minlength="1" minlength="1"
maxlength="4000" maxlength="1000"
required="true" required="true"
rows="5" rows="5"
></textarea> ></textarea>
</div> </div>
</div>
<div id="${this.idKey}-banner" class="inline-banner" aria-live="polite"> <div id="${this.idKey}-banner" class="inline-banner" aria-live="polite">
<gw-icon iconKey="circle-info"></gw-icon> <gw-icon iconKey="circle-info" title="info"></gw-icon>
<p>Comments are manually approved</p> <span>Comments are manually approved</span>
</div> </div>
<div class="form-footer"> <div class="form-footer">
<input id="${this.idKey}-reset" type="reset" value="Reset"> <input id="${this.idKey}-reset" type="reset" value="Reset">
@ -162,13 +135,11 @@ registerNamespace("GW.Controls", function (ns)
} }
//#region Handlers //#region Handlers
registerHandlers() registerHandlers() {
{
this.formEl.onsubmit = this.onSubmit; this.formEl.onsubmit = this.onSubmit;
} }
onSubmit = (event) => onSubmit = (event) => {
{
event.preventDefault(); event.preventDefault();
const contentObj = { const contentObj = {
@ -176,12 +147,13 @@ registerNamespace("GW.Controls", function (ns)
email: this.emailInpt.value, email: this.emailInpt.value,
website: this.websiteInpt.value, website: this.websiteInpt.value,
responseTo: this.respToInpt.value, responseTo: this.respToInpt.value,
comment: this.commentInpt.value, comment: (
this.commentInpt.value || ""
).replaceAll("\n", "<br>").replaceAll("(", "\\("),
timestamp: new Date().toUTCString(), timestamp: new Date().toUTCString(),
}; };
const contentAry = []; const contentAry = [];
for (let contentKey in contentObj) for (let contentKey in contentObj) {
{
contentAry.push(`${contentKey}=${contentObj[contentKey]}`); contentAry.push(`${contentKey}=${contentObj[contentKey]}`);
} }
@ -193,33 +165,33 @@ registerNamespace("GW.Controls", function (ns)
); );
request.setRequestHeader("Content-Type", "application/json"); request.setRequestHeader("Content-Type", "application/json");
request.onreadystatechange = () => request.onreadystatechange = () => {
{
if (request.readyState !== XMLHttpRequest.DONE) { return; } if (request.readyState !== XMLHttpRequest.DONE) { return; }
if (Math.floor(request.status / 100) !== 2) if (Math.floor(request.status / 100) !== 2) {
{
console.log(request.responseText); console.log(request.responseText);
this.bannerEl.classList.add("warning"); this.bannerEl.classList.add("warning");
this.bannerEl.innerHTML = this.bannerEl.innerHTML =
` `
<gw-icon iconKey="triangle-exclamation"></gw-icon> <gw-icon iconKey="triangle-exclamation" title="warning"></gw-icon>
<span> <span>
That didn't work. That didn't work.
${this.fallbackEmail ${this.fallbackEmail
? `<a class="full" href="mailto:${this.fallbackEmail}?subject=Comment on ${document.title}&body=${contentAry.join("; ")}">Send your comment as an email instead</a>.` ? `<a class="full" href="mailto:${this.fallbackEmail}?subject=Comment on ${document.title}&body=${contentAry.join("; ")}">Click here to send as an email instead</a>.`
: "" : ""
} }
</span> </span>
`; `;
} }
else else {
{
alert("Your comment has been submitted!"); alert("Your comment has been submitted!");
} }
}; };
request.send(JSON.stringify({ content: contentAry.join("; ") })); 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-name", contentObj.name);
localStorage.setItem("comment-email", contentObj.email); localStorage.setItem("comment-email", contentObj.email);
@ -234,12 +206,11 @@ registerNamespace("GW.Controls", function (ns)
}; };
customElements.define("gw-comment-form", ns.CommentForm); customElements.define("gw-comment-form", ns.CommentForm);
ns.CommentList = class CommentList extends HTMLElement ns.CommentList = class CommentList extends HTMLElement {
{
//#region staticProperties //#region staticProperties
static observedAttributes = [];
static instanceCount = 0; static instanceCount = 0;
static instanceMap = {}; static instanceMap = {};
static Data = [];
//#endregion //#endregion
//#region instance properties //#region instance properties
@ -254,21 +225,18 @@ registerNamespace("GW.Controls", function (ns)
//#endregion //#endregion
//#endregion //#endregion
constructor() constructor() {
{
super(); super();
this.instanceId = CommentList.instanceCount++; this.instanceId = CommentList.instanceCount++;
CommentList.instanceMap[this.instanceId] = this; CommentList.instanceMap[this.instanceId] = this;
CommentList.Data[this.instanceId] = {};
} }
get idKey() get idKey() {
{
return `gw-comment-list-${this.instanceId}`; return `gw-comment-list-${this.instanceId}`;
} }
//#region HTMLElement implementation connectedCallback() {
connectedCallback()
{
if (this.isInitialized) { return; } if (this.isInitialized) { return; }
this.gSpreadsheetId = this.getAttribute("gSpreadsheetId"); this.gSpreadsheetId = this.getAttribute("gSpreadsheetId");
@ -280,10 +248,8 @@ registerNamespace("GW.Controls", function (ns)
this.isInitialized = true; this.isInitialized = true;
} }
//#endregion
async loadAndRender() async loadAndRender() {
{
this.innerHTML = ` this.innerHTML = `
<div class="inline-banner"> <div class="inline-banner">
<gw-icon iconkey="circle-info" title="info"></gw-icon> <gw-icon iconkey="circle-info" title="info"></gw-icon>
@ -292,12 +258,11 @@ registerNamespace("GW.Controls", function (ns)
` `
const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId); const sheetReader = new GW.Gizmos.GoogleSheetsReader(this.gSpreadsheetId, this.gSheetId);
const sheetData = await sheetReader.loadData(); await sheetReader.loadData();
this.innerHTML = ""; this.innerHTML = "";
const allComments = sheetReader.rowData; const allComments = sheetReader.rowData;
if (this.isNewestFirst) if (this.isNewestFirst) {
{
allComments.reverse(); allComments.reverse();
} }
@ -307,72 +272,59 @@ registerNamespace("GW.Controls", function (ns)
const allCommentsIndex = {}; const allCommentsIndex = {};
const topLevelCommentIdxs = []; const topLevelCommentIdxs = [];
const childCommentIdxs = []; const childCommentIdxs = [];
for (let i = 0; i < allComments.length; i++) for (let i = 0; i < allComments.length; i++) {
{
const comment = allComments[i]; const comment = allComments[i];
allCommentsIndex[comment.ID] = i; allCommentsIndex[comment.ID] = i;
if (!comment.ResponseTo) if (!comment.ResponseTo) {
{
topLevelCommentIdxs.push(i); topLevelCommentIdxs.push(i);
} }
else else {
{
childCommentIdxs.push(i); childCommentIdxs.push(i);
} }
} }
childCommentIdxs.forEach(childIdx => childCommentIdxs.forEach(childIdx => {
{
const replyId = allComments[childIdx].ResponseTo; const replyId = allComments[childIdx].ResponseTo;
const respondeeComment = allComments[allCommentsIndex[replyId]]; const respondeeComment = allComments[allCommentsIndex[replyId]];
respondeeComment.childrenIdxs = respondeeComment.childrenIdxs || []; respondeeComment.ChildIdxs = respondeeComment.ChildIdxs || [];
respondeeComment.childrenIdxs.push(childIdx); respondeeComment.ChildIdxs.push(childIdx);
}); });
let commentsToBuild = []; let commentsToBuild = [];
topLevelCommentIdxs.forEach( topLevelCommentIdxs.forEach(
topCommentIdx => commentsToBuild.push( topCommentIdx => commentsToBuild.push({
{
parent: this.containerEl, parent: this.containerEl,
parentId: null,
comment: allComments[topCommentIdx] comment: allComments[topCommentIdx]
} })
)
); );
while (commentsToBuild.length > 0) while (commentsToBuild.length > 0) {
{ let { parent, comment } = commentsToBuild.shift();
let { parent, parentId, comment } = commentsToBuild.shift(); if (!comment.Timestamp) {
if (!comment.Timestamp)
{
continue; continue;
} }
CommentList.Data[this.instanceId][comment.ID] = comment;
parent.insertAdjacentHTML("beforeend", ` parent.insertAdjacentHTML("beforeend", `
<gw-comment-card id="${this.idKey}-cmt-${comment.ID}" <gw-comment-card id="${this.idKey}-cmt-${comment.ID}"
commentId="${comment.ID || ""}" listInstance=${this.instanceId}
replyToId="${parentId || ""}" commentId=${comment.ID}
numChildren="${(comment.childrenIdxs || []).length}" gwCommentFormId=${this.gwCommentFormId || ""}
commenterName="${comment["Display Name"] || ""}"
isoTimestamp="${comment.Timestamp.toISOString()}"
websiteURL="${comment.Website || ""}"
commentText="${comment.Comment || ""}"
gwCommentFormId="${this.gwCommentFormId || ""}"
></gw-comment-card> ></gw-comment-card>
`); `);
const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`); const commentEl = document.getElementById(`${this.idKey}-cmt-${comment.ID}`);
(comment.childrenIdxs || []).forEach( (comment.ChildIdxs || []).forEach(
childIdx => commentsToBuild.push({ childIdx => commentsToBuild.push({
parent: commentEl.articleEl, parent: commentEl.articleEl,
parentId: comment.ID,
comment: allComments[childIdx] comment: allComments[childIdx]
}) })
); );
} }
} }
renderContent() renderContent() {
{
//Markup //Markup
this.innerHTML = ` this.innerHTML = `
<div id="${this.idKey}-container" class="comments-container""> <div id="${this.idKey}-container" class="comments-container"">
@ -384,17 +336,14 @@ registerNamespace("GW.Controls", function (ns)
} }
//#region Handlers //#region Handlers
registerHandlers() registerHandlers() {
{
} }
//#endregion //#endregion
}; };
customElements.define("gw-comment-list", ns.CommentList); customElements.define("gw-comment-list", ns.CommentList);
ns.CommentCard = class CommentCard extends HTMLElement ns.CommentCard = class CommentCard extends HTMLElement {
{
//#region staticProperties //#region staticProperties
static observedAttributes = [];
static instanceCount = 0; static instanceCount = 0;
static instanceMap = {}; static instanceMap = {};
//#endregion //#endregion
@ -402,15 +351,16 @@ registerNamespace("GW.Controls", function (ns)
//#region instance properties //#region instance properties
instanceId; instanceId;
isInitialized; isInitialized;
commentId; commentId;
gwCommentFormId;
replyToId; replyToId;
numChildren; numChildren;
commenterName; commenterName;
isoTimestamp;
datetime; datetime;
websiteURL; websiteURL;
commentText; commentText;
gwCommentFormId;
//#region element properties //#region element properties
articleEl; articleEl;
@ -418,33 +368,32 @@ registerNamespace("GW.Controls", function (ns)
//#endregion //#endregion
//#endregion //#endregion
constructor() constructor() {
{
super(); super();
this.instanceId = CommentCard.instanceCount++; this.instanceId = CommentCard.instanceCount++;
CommentCard.instanceMap[this.instanceId] = this; CommentCard.instanceMap[this.instanceId] = this;
} }
get idKey() get idKey() {
{
return `gw-comment-card-${this.instanceId}`; return `gw-comment-card-${this.instanceId}`;
} }
//#region HTMLElement implementation //#region HTMLElement implementation
connectedCallback() connectedCallback() {
{
if (this.isInitialized) { return; } if (this.isInitialized) { return; }
this.commentId = this.getAttribute("commentId"); 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.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.renderContent();
this.registerHandlers(); this.registerHandlers();
@ -452,12 +401,10 @@ registerNamespace("GW.Controls", function (ns)
} }
//#endregion //#endregion
renderContent() renderContent() {
{
let headerText = this.replyToId let headerText = this.replyToId
? `Comment #${this.commentId} replying to #${this.replyToId}` ? `Comment #${this.commentId} replying to #${this.replyToId}`
: `Top level comment #${this.commentId}`; : `Top level comment #${this.commentId}`;
headerText += ` with ${this.numChildren} direct ${this.numChildren == 1 ? "reply" : "replies"}`; headerText += ` with ${this.numChildren} direct ${this.numChildren == 1 ? "reply" : "replies"}`;
const displayTimestamp = this.datetime.toLocaleString( const displayTimestamp = this.datetime.toLocaleString(
@ -482,7 +429,7 @@ registerNamespace("GW.Controls", function (ns)
${commenterNameEl} ${commenterNameEl}
<div class="comment-header-right"> <div class="comment-header-right">
<time id="${this.idKey}-timestamp" <time id="${this.idKey}-timestamp"
datetime="${this.isoTimestamp}" datetime="${this.datetime.toISOString()}"
tabindex="-1" tabindex="-1"
>${displayTimestamp}</time> >${displayTimestamp}</time>
<button id="${this.idKey}-show" class="show-comment">Show #${this.commentId}</button> <button id="${this.idKey}-show" class="show-comment">Show #${this.commentId}</button>
@ -505,19 +452,16 @@ registerNamespace("GW.Controls", function (ns)
} }
//#region Handlers //#region Handlers
registerHandlers() registerHandlers() {
{
this.replyBtn.onclick = this.onReply; this.replyBtn.onclick = this.onReply;
this.hideBtn.onclick = this.onHide; this.hideBtn.onclick = this.onHide;
this.showBtn.onclick = this.onShow; this.showBtn.onclick = this.onShow;
} }
onReply = () => onReply = () => {
{
const gwCommentForm = document.getElementById(this.gwCommentFormId); const gwCommentForm = document.getElementById(this.gwCommentFormId);
const respToInpt = gwCommentForm.respToInpt; const respToInpt = gwCommentForm.respToInpt;
if (!respToInpt) if (!respToInpt) {
{
alert("Comment form not found"); alert("Comment form not found");
return; return;
} }
@ -526,18 +470,59 @@ registerNamespace("GW.Controls", function (ns)
respToInpt.focus(); respToInpt.focus();
}; };
onHide = () => onHide = () => {
{
this.classList.add("collapsed"); this.classList.add("collapsed");
this.showBtn.focus(); this.showBtn.focus();
}; };
onShow = () => onShow = () => {
{
this.classList.remove("collapsed"); this.classList.remove("collapsed");
this.timestamp.focus(); this.timestamp.focus();
}; };
//#endregion //#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 += `<a href="${linkURL}" target="_blank">${linkText}</a>`;
linkObj = {};
}
else {
commentText += char;
}
}
return commentText;
}
}; };
customElements.define("gw-comment-card", ns.CommentCard); customElements.define("gw-comment-card", ns.CommentCard);
}); }) (window.GW.Controls = window.GW.Controls || {});

View File

@ -12,7 +12,7 @@ eleventyComputed:
<p class="center-text">Guestbook Archive: <a href="https://web.archive.org/web/20240528231121/https%3A%2F%2Fleilukin.123guestbook.com%2F">123Guestbook</a></p> <p class="center-text">Guestbook Archive: <a href="https://web.archive.org/web/20240528231121/https%3A%2F%2Fleilukin.123guestbook.com%2F">123Guestbook</a></p>
<p>Feel free to say hi and connect with me!</p> <p>Feel free to say hi and connect with me! The comment field supports <a href="https://www.markdownguide.org/basic-syntax/#links">Markdown links</a> and and HTML.</p>
<p>JavaScript is required for this guestbook to function. Special thanks to <a href="https://groundedwren.neocities.org/pages/controls/guestbookDemo">Vera Konigin</a> for this guestbook widget!</p> <p>JavaScript is required for this guestbook to function. Special thanks to <a href="https://groundedwren.neocities.org/pages/controls/guestbookDemo">Vera Konigin</a> for this guestbook widget!</p>